Wie verwende ich GROUP BY, um Zeichenfolgen in SQL Server zu verketten?

373

Wie bekomme ich:

id       Name       Value
1          A          4
1          B          8
2          C          9

zu

id          Column
1          A:4, B:8
2          C:9
Eldila
quelle
18
Diese Art von Problem lässt sich unter MySQL mit seiner GROUP_CONCAT()Aggregatfunktion leicht lösen , aber das Lösen unter Microsoft SQL Server ist umständlicher. Siehe die folgende SO-Frage für Hilfe: " Wie bekomme ich mehrere Datensätze gegen einen Datensatz basierend auf der Beziehung? "
Bill Karwin
1
Jeder mit einem Microsoft-Konto sollte für eine einfachere Lösung für connect stimmen
Jens Mühlenhoff
1
Sie können die hier gefundenen SQLCLR-Aggregate als Ersatz verwenden, bis T-SQL erweitert wird: groupconcat.codeplex.com
Orlando Colamatteo
1
Duplikat von stackoverflow.com/questions/194852/…
Salman A

Antworten:

550

Kein CURSOR, WHILE-Loop oder benutzerdefinierte Funktion erforderlich .

Sie müssen nur mit FOR XML und PATH kreativ sein.

[Hinweis: Diese Lösung funktioniert nur unter SQL 2005 und höher. In der ursprünglichen Frage wurde die verwendete Version nicht angegeben.]

CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT 
  [ID],
  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID

DROP TABLE #YourTable
Kevin Fairchild
quelle
6
warum sollte man einen temporären Tisch nicht sperren?
Amy B
3
Dies ist die coolste SQL-Sache, die ich in meinem Leben gesehen habe. Irgendeine Idee, ob es für große Datenmengen "schnell" ist? Es kriecht nicht wie ein Cursor oder so, oder? Ich wünschte, mehr Leute würden diese Verrücktheit abstimmen.
user12861
6
Eh. Ich hasse nur den Unterabfragestil. JOINS sind so viel schöner. Glaube nur nicht, dass ich das in dieser Lösung nutzen kann. Wie auch immer, ich bin froh zu sehen, dass es hier außer mir noch andere SQL-Idioten gibt, die gerne solche Dinge lernen. Ein
Kevin Fairchild
6
Eine etwas sauberere Methode zur Zeichenfolgenmanipulation: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Wert] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')), 1,2, '') AS NameValues
Jonathan Sayce
3
Nur um etwas zu notieren, das ich gefunden habe. Selbst in einer Umgebung ohne Berücksichtigung der Groß- und Kleinschreibung muss der .value-Teil der Abfrage in Kleinbuchstaben geschrieben werden. Ich
vermute
136

Wenn es sich um SQL Server 2017 oder SQL Server Vnext, SQL Azure handelt, können Sie string_agg wie folgt verwenden:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id
Kannan Kandasamy
quelle
Funktioniert einwandfrei!
Argoo
1
Das funktioniert super, besser als die akzeptierte Antwort.
Jannick Breunis
51

Die Verwendung des XML-Pfads wird nicht wie erwartet perfekt verkettet. Er ersetzt "&" durch "& amp;" und wird auch mit <" and "> ... vielleicht ein paar anderen Dingen herumspielen, nicht sicher ... aber Sie können dies versuchen

Ich bin auf eine Problemumgehung gestoßen ... Sie müssen Folgendes ersetzen:

FOR XML PATH('')
)

mit:

FOR XML PATH(''),TYPE
).value('(./text())[1]','VARCHAR(MAX)')

... oder NVARCHAR(MAX)wenn du das benutzt.

Warum zum Teufel hat keine SQLverkettete Aggregatfunktion? Das ist eine PITA.

Allen
quelle
2
Ich habe das Netz nach dem besten Weg durchsucht, die Ausgabe NICHT zu codieren. Ich danke dir sehr! Dies ist die endgültige Antwort - bis MS dies wie eine CONCAT () - Aggregatfunktion angemessen unterstützt. Ich werfe dies in ein Outer-Apply, das mein verkettetes Feld zurückgibt. Ich bin kein Fan davon, verschachtelte Auswahlen zu meinen Auswahlanweisungen hinzuzufügen.
MikeTeeVee
Ich stimmte zu, ohne Value zu verwenden, können Probleme auftreten, bei denen der Text ein XML-codiertes Zeichen ist. In meinem Blog finden Sie Szenarien für die gruppierte Verkettung in SQL Server. blog.vcillusion.co.in/…
vCillusion
40

Ich lief in ein paar Probleme , als ich versuchte Kevin Fairchilds Vorschlag zur Arbeit Umwandlung mit Streichern Leer- und Sonder XML - Zeichen enthalten ( &, <, >) , die codiert wurden.

Die endgültige Version meines Codes (die die ursprüngliche Frage nicht beantwortet, aber für jemanden nützlich sein kann) sieht folgendermaßen aus:

CREATE TABLE #YourTable ([ID] INT, [Name] VARCHAR(MAX), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'Oranges & Lemons',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'1 < 2',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT  [ID],
  STUFF((
    SELECT ', ' + CAST([Name] AS VARCHAR(MAX))
    FROM #YourTable WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE 
     /* Use .value to uncomment XML entities e.g. &gt; &lt; etc*/
    ).value('.','VARCHAR(MAX)') 
  ,1,2,'') as NameValues
FROM    #YourTable Results
GROUP BY ID

DROP TABLE #YourTable

Anstatt ein Leerzeichen als Trennzeichen zu verwenden und alle Leerzeichen durch Kommas zu ersetzen, werden jedem Wert nur ein Komma und ein Leerzeichen vorangestellt und dann STUFFdie ersten beiden Zeichen entfernt.

Die XML-Codierung wird automatisch mithilfe der TYPE- Direktive durchgeführt.

Jonathan Sayce
quelle
21

Eine weitere Option mit SQL Server 2005 und höher

---- test data
declare @t table (OUTPUTID int, SCHME varchar(10), DESCR varchar(10))
insert @t select 1125439       ,'CKT','Approved'
insert @t select 1125439       ,'RENO','Approved'
insert @t select 1134691       ,'CKT','Approved'
insert @t select 1134691       ,'RENO','Approved'
insert @t select 1134691       ,'pn','Approved'

---- actual query
;with cte(outputid,combined,rn)
as
(
  select outputid, SCHME + ' ('+DESCR+')', rn=ROW_NUMBER() over (PARTITION by outputid order by schme, descr)
  from @t
)
,cte2(outputid,finalstatus,rn)
as
(
select OUTPUTID, convert(varchar(max),combined), 1 from cte where rn=1
union all
select cte2.outputid, convert(varchar(max),cte2.finalstatus+', '+cte.combined), cte2.rn+1
from cte2
inner join cte on cte.OUTPUTID = cte2.outputid and cte.rn=cte2.rn+1
)
select outputid, MAX(finalstatus) from cte2 group by outputid
Cyberkiwi
quelle
Vielen Dank für die Eingabe. Ich bevorzuge immer die Verwendung von CTEs und rekursiven CTEs, um Probleme in SQL Server zu lösen. Das ist funktioniert man arbeitet für mich super!
Gbdavid
Ist es möglich, es in einer Abfrage mit Outer Apply zu verwenden?
Feuer im Loch
14

Installieren Sie die SQLCLR-Aggregate von http://groupconcat.codeplex.com

Dann können Sie Code wie diesen schreiben, um das gewünschte Ergebnis zu erhalten:

CREATE TABLE foo
(
 id INT,
 name CHAR(1),
 Value CHAR(1)
);

INSERT  INTO dbo.foo
    (id, name, Value)
VALUES  (1, 'A', '4'),
        (1, 'B', '8'),
        (2, 'C', '9');

SELECT  id,
    dbo.GROUP_CONCAT(name + ':' + Value) AS [Column]
FROM    dbo.foo
GROUP BY id;
Orlando Colamatteo
quelle
Ich habe es vor ein paar Jahren verwendet, die Syntax ist viel sauberer als alle "XML Path" -Tricks und es funktioniert sehr gut. Ich empfehle es dringend, wenn SQL CLR-Funktionen eine Option sind.
Nach dem
12

Mit SQL Server 2005 und höher können Sie Ihre eigenen benutzerdefinierten Aggregatfunktionen erstellen , einschließlich Informationen zur Verkettung. Weitere Informationen finden Sie im Beispiel am Ende des verknüpften Artikels.

Joel Coehoorn
quelle
4
Leider erfordert dies (?) Die Verwendung von CLR-Assemblys. Dies ist ein weiteres
1
Nur das Beispiel verwendet CLR für die eigentliche Verkettungsimplementierung, dies ist jedoch nicht erforderlich. Sie könnten dafür sorgen, dass die Verkettungsaggregatfunktion FOR XML verwendet, sodass es zumindest in Zukunft besser ist, sie aufzurufen!
Shiv
12

Acht Jahre später ... Microsoft SQL Server vNext Database Engine hat Transact-SQL endlich erweitert, um die Verkettung gruppierter Zeichenfolgen direkt zu unterstützen. In der Community Technical Preview Version 1.0 wurde die Funktion STRING_AGG und in CTP 1.1 die Klausel WITHIN GROUP für die Funktion STRING_AGG hinzugefügt.

Referenz: https://msdn.microsoft.com/en-us/library/mt775028.aspx

Shem Sargent
quelle
9

Dies ist nur eine Ergänzung zu Kevin Fairchilds Beitrag (übrigens sehr klug). Ich hätte es als Kommentar hinzugefügt, aber ich habe noch nicht genug Punkte :)

Ich benutzte diese Idee für eine Ansicht, an der ich arbeitete, aber die Gegenstände, die ich konzentrierte, enthielten Leerzeichen. Deshalb habe ich den Code leicht geändert, um keine Leerzeichen als Trennzeichen zu verwenden.

Nochmals vielen Dank für die coole Problemumgehung Kevin!

CREATE TABLE #YourTable ( [ID] INT, [Name] CHAR(1), [Value] INT ) 

INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'A', 4) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'B', 8) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (2, 'C', 9) 

SELECT [ID], 
       REPLACE(REPLACE(REPLACE(
                          (SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) as A 
                           FROM   #YourTable 
                           WHERE  ( ID = Results.ID ) 
                           FOR XML PATH (''))
                        , '</A><A>', ', ')
                ,'<A>','')
        ,'</A>','') AS NameValues 
FROM   #YourTable Results 
GROUP  BY ID 

DROP TABLE #YourTable 
Phillip
quelle
9

Ein Beispiel wäre

In Oracle können Sie die LISTAGG-Aggregatfunktion verwenden.

Originalaufzeichnungen

name   type
------------
name1  type1
name2  type2
name2  type3

Sql

SELECT name, LISTAGG(type, '; ') WITHIN GROUP(ORDER BY name)
FROM table
GROUP BY name

Ergebnis in

name   type
------------
name1  type1
name2  type2; type3
Michal B.
quelle
6
Sieht gut aus, aber die Fragen beziehen sich speziell nicht auf Oracle.
user12861
13
Ich verstehe. Aber ich suchte das Gleiche für Oracle, also dachte ich, ich würde es hier für andere Leute wie mich platzieren :)
Michal B.
@ MichaelB. Fehlt Ihnen nicht die interne Syntax? zB: listagg (Typ, ',') innerhalb der Gruppe (Reihenfolge nach Namen)?
Gregory
@Gregory: Ich habe meine Antwort bearbeitet. Ich glaube, meine alte Lösung hat früher funktioniert. Das aktuelle Formular, das Sie vorgeschlagen haben, wird auf jeden Fall funktionieren, danke.
Michal B.
1
für zukünftige Leute - Sie können eine neue Frage mit Ihrer eigenen Antwort für einen signifikanten Unterschied wie andere Plattform schreiben
Mike M
7

Diese Art von Frage wird hier sehr oft gestellt, und die Lösung wird stark von den zugrunde liegenden Anforderungen abhängen:

https://stackoverflow.com/search?q=sql+pivot

und

https://stackoverflow.com/search?q=sql+concatenate

Normalerweise gibt es keine reine SQL-Möglichkeit, dies ohne dynamisches SQL, eine benutzerdefinierte Funktion oder einen Cursor zu tun.

Cade Roux
quelle
2
Nicht wahr. Die Lösung von cyberkiwi mit cte: s ist reines SQL ohne herstellerspezifische Hackerei.
Björn Lindqvist
1
Zum Zeitpunkt der Frage und Antwort hätte ich rekursive CTEs nicht als schrecklich portabel gezählt, aber sie werden jetzt von Oracle unterstützt. Die beste Lösung hängt von der Plattform ab. Bei SQL Server handelt es sich höchstwahrscheinlich um die FOR XML-Technik oder ein Kunden-CLR-Aggregat.
Cade Roux
1
die ultimative Antwort auf alle Fragen? stackoverflow.com/search?q=[was auch immer die Frage]
Junchen Liu
7

Nur um das zu ergänzen, was Cade gesagt hat, ist dies normalerweise eine Front-End-Anzeige und sollte daher dort behandelt werden. Ich weiß, dass es manchmal einfacher ist, etwas 100% in SQL für Dinge wie den Dateiexport oder andere "Nur SQL" -Lösungen zu schreiben, aber meistens sollte diese Verkettung in Ihrer Anzeigeebene behandelt werden.

Tom H.
quelle
11
Gruppierung ist jetzt eine Front-End-Anzeige? Es gibt viele gültige Szenarien zum Verketten einer Spalte in einer gruppierten Ergebnismenge.
MGOwen
5

Benötigen Sie keinen Cursor ... eine while-Schleife ist ausreichend.

------------------------------
-- Setup
------------------------------

DECLARE @Source TABLE
(
  id int,
  Name varchar(30),
  Value int
)

DECLARE @Target TABLE
(
  id int,
  Result varchar(max) 
)


INSERT INTO @Source(id, Name, Value) SELECT 1, 'A', 4
INSERT INTO @Source(id, Name, Value) SELECT 1, 'B', 8
INSERT INTO @Source(id, Name, Value) SELECT 2, 'C', 9


------------------------------
-- Technique
------------------------------

INSERT INTO @Target (id)
SELECT id
FROM @Source
GROUP BY id

DECLARE @id int, @Result varchar(max)
SET @id = (SELECT MIN(id) FROM @Target)

WHILE @id is not null
BEGIN
  SET @Result = null

  SELECT @Result =
    CASE
      WHEN @Result is null
      THEN ''
      ELSE @Result + ', '
    END + s.Name + ':' + convert(varchar(30),s.Value)
  FROM @Source s
  WHERE id = @id

  UPDATE @Target
  SET Result = @Result
  WHERE id = @id

  SET @id = (SELECT MIN(id) FROM @Target WHERE @id < id)
END

SELECT *
FROM @Target
Amy B.
quelle
@marc_s Vielleicht ist eine bessere Kritik, dass PRIMARY KEY für die Tabellenvariablen deklariert werden sollte.
Amy B
@marc_s Bei näherer Betrachtung ist dieser Artikel eine Täuschung - ebenso wie fast alle Diskussionen über die Leistung ohne E / A-Messung. Ich habe etwas über LAG gelernt - also danke dafür.
Amy B
4

Lassen Sie uns ganz einfach werden:

SELECT stuff(
    (
    select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb 
    FOR XML PATH('')
    )
, 1, 2, '')

Ersetzen Sie diese Zeile:

select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb

Mit Ihrer Anfrage.

Marquinho Peli
quelle
3

Es wurden keine Kreuzantworten angezeigt, und es ist auch keine XML-Extraktion erforderlich. Hier ist eine etwas andere Version dessen, was Kevin Fairchild geschrieben hat. Es ist schneller und einfacher in komplexeren Abfragen zu verwenden:

   select T.ID
,MAX(X.cl) NameValues
 from #YourTable T
 CROSS APPLY 
 (select STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
    FROM #YourTable 
    WHERE (ID = T.ID) 
    FOR XML PATH(''))
  ,1,2,'')  [cl]) X
  GROUP BY T.ID
Mordechai
quelle
1
Ohne die Verwendung von Value können Probleme auftreten, bei denen der Text ein XML-codiertes Zeichen ist
vCillusion
2

Sie können die Leistung auf folgende Weise erheblich verbessern, wenn die Gruppierung nach hauptsächlich ein Element enthält:

SELECT 
  [ID],

CASE WHEN MAX( [Name]) = MIN( [Name]) THEN 
MAX( [Name]) NameValues
ELSE

  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues

END

FROM #YourTable Results
GROUP BY ID
Eduard
quelle
Angenommen, Sie möchten keine doppelten Namen in der Liste haben, die Sie möglicherweise oder möglicherweise nicht.
jnm2
1

Verwenden der Ersetzungsfunktion und des FOR JSON-Pfads

SELECT T3.DEPT, REPLACE(REPLACE(T3.ENAME,'{"ENAME":"',''),'"}','') AS ENAME_LIST
FROM (
 SELECT DEPT, (SELECT ENAME AS [ENAME]
        FROM EMPLOYEE T2
        WHERE T2.DEPT=T1.DEPT
        FOR JSON PATH,WITHOUT_ARRAY_WRAPPER) ENAME
    FROM EMPLOYEE T1
    GROUP BY DEPT) T3

Für Beispieldaten und weitere Möglichkeiten klicken Sie hier

Mahesh
quelle
1

Wenn Sie clr aktiviert haben, können Sie die Group_Concat- Bibliothek von GitHub verwenden

Manfred Wippel
quelle