Come utilizzare GROUP BY per concatenare le stringhe in SQL Server?


373

Come ottengo:

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

per

id          Column
1          A:4, B:8
2          C:9

18
Questo tipo di problema viene risolto facilmente su MySQL con la sua GROUP_CONCAT()funzione aggregata, ma risolverlo su Microsoft SQL Server è più imbarazzante. Vedere la seguente domanda SO di aiuto: " Come ottenere più record contro un record basato sulla relazione? "
Bill Karwin,

1
Chiunque disponga di un account microsoft dovrebbe votare una soluzione più semplice su connect: connect.microsoft.com/SQLServer/feedback/details/427987/…
Jens Mühlenhoff

1
È possibile utilizzare gli aggregati SQLCLR trovati qui come sostituti fino a quando T-SQL non viene migliorato: groupconcat.codeplex.com
Orlando Colamatteo

Risposte:


550

Nessun CURSORE, ciclo WHILE o funzione definita dall'utente necessari .

Devo solo essere creativo con FOR XML e PATH.

[Nota: questa soluzione funziona solo su SQL 2005 e versioni successive. La domanda originale non specificava la versione in uso.]

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

6
perché uno dovrebbe abbandonare un tavolo temporaneo?
Amy B,

3
Questa è la cosa SQL più bella che abbia mai visto in vita mia. Qualche idea se è "veloce" per grandi set di dati? Non inizia a gattonare come farebbe un cursore o altro, vero? Vorrei che più persone votassero questa follia.
user12861

6
Eh. Odio solo lo stile di una query secondaria. I JOIN sono molto più belli. Non pensare di poterlo utilizzare in questa soluzione. Ad ogni modo, sono felice di vedere che ci sono altri dorks SQL qui oltre a me a cui piace imparare cose come questa. Complimenti a tutti :)
Kevin Fairchild,

6
Un modo leggermente più pulito di manipolare le stringhe: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) DA #YourTable DOVE (ID = Results.ID) PER XML PERCORSO ('')), 1,2, '') AS NameValues
Jonathan Sayce,

3
Solo per notare qualcosa che ho trovato. Anche in un ambiente senza distinzione tra maiuscole e minuscole, la parte .value della query DEVE essere minuscola. Immagino che sia perché è XML, che fa distinzione tra maiuscole
Jaloopa il

136

Se si tratta di SQL Server 2017 o SQL Server Vnext, SQL Azure è possibile utilizzare string_agg come di seguito:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id

Funziona perfettamente!
Argoo,

1
Funziona alla grande, meglio della risposta accettata.
Jannick Breunis,

51

l'utilizzo del percorso XML non si concatenerà perfettamente come ci si potrebbe aspettare ... sostituirà "&" con "& amp;" e rovineremo anche <" and "> ... forse qualche altra cosa, non sono sicuro ... ma puoi provare questo

Mi sono imbattuto in una soluzione alternativa per questo ... è necessario sostituire:

FOR XML PATH('')
)

con:

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

... o NVARCHAR(MAX)se è quello che stai usando.

perché diavolo non SQLha una funzione aggregata concatenata? questa è una PITA.


2
Ho cercato la rete cercando il modo migliore per NON codificare l'output. Grazie mille! Questa è la risposta definitiva - fino a quando MS non aggiungerà il supporto adeguato per questo, come una funzione aggregata CONCAT (). Quello che faccio è lanciarlo in un'applicazione esterna che restituisce il mio campo concatenato. Non sono un fan di aggiungere selezioni nidificate nelle mie dichiarazioni select.
MikeTeeVee,

Ho accettato, senza usare Value, possiamo incorrere in problemi in cui il testo è un carattere codificato XML. Si prega di trovare il mio blog sugli scenari per la concatenazione raggruppata in SQL Server. blog.vcillusion.co.in/…
vCillusion

40

Mi sono imbattuto in un paio di problemi, quando ho provato a convertire il suggerimento di Kevin Fairchild per il lavoro con le stringhe contenenti spazi e caratteri speciali XML ( &, <, >) che sono stati codificati.

La versione finale del mio codice (che non risponde alla domanda originale ma può essere utile a qualcuno) è simile alla seguente:

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

Anziché utilizzare uno spazio come delimitatore e sostituire tutti gli spazi con virgole, anticipa semplicemente una virgola e uno spazio per ciascun valore, quindi utilizza STUFFper rimuovere i primi due caratteri.

La codifica XML viene gestita automaticamente utilizzando la direttiva TYPE .


21

Un'altra opzione che utilizza SQL Server 2005 e versioni successive

---- 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

Grazie per l'input, preferisco sempre usare CTE e CTE ricorsivi per risolvere i problemi nel server SQL. Questo ha funzionato per me funziona alla grande!
gbdavid,

è possibile utilizzarlo in una query con applicazione esterna?
fuoco nella buca,

14

Installare gli aggregati SQLCLR da http://groupconcat.codeplex.com

Quindi puoi scrivere codice in questo modo per ottenere il risultato che hai richiesto:

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;

L'ho usato qualche anno fa, la sintassi è molto più pulita di tutti i trucchi "XML Path" e funziona molto bene. Lo consiglio vivamente quando le funzioni CLR SQL sono un'opzione.
AFract,

12

SQL Server 2005 e versioni successive ti consentono di creare le tue funzioni di aggregazione personalizzate , anche per cose come la concatenazione. Vedi l'esempio in fondo all'articolo collegato.


4
Sfortunatamente questo richiede (?) L'uso di assembly CLR .. che è un altro problema da affrontare: - /

1
Solo l'esempio utilizza CLR per l'implementazione effettiva della concatenazione, ma ciò non è necessario. Potresti fare in modo che la funzione di aggregazione della concatenazione utilizzi FOR XML, quindi almeno è meglio chiamarla in futuro!
Shiv,

12

Otto anni dopo ... Microsoft SQL Server vNext Database Engine ha finalmente migliorato Transact-SQL per supportare direttamente la concatenazione di stringhe raggruppate. L'anteprima tecnica della comunità versione 1.0 ha aggiunto la funzione STRING_AGG e CTP 1.1 ha aggiunto la clausola WITHIN GROUP per la funzione STRING_AGG.

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


9

Questa è solo un'aggiunta al post di Kevin Fairchild (molto intelligente tra l'altro). Lo avrei aggiunto come commento, ma non ho ancora abbastanza punti :)

Stavo usando questa idea per una vista su cui stavo lavorando, tuttavia gli elementi che stavo concatenando contenevano spazi. Quindi ho modificato leggermente il codice per non usare spazi come delimitatori.

Ancora grazie per la bella soluzione 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 

9

Un esempio sarebbe

In Oracle è possibile utilizzare la funzione aggregata LISTAGG.

Record originali

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

Sql

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

Risultato in

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

6
Sembra carino, ma le domande non riguardano specificamente Oracle.
user12861

13
Capisco. Ma stavo cercando la stessa cosa per Oracle, quindi ho pensato di metterlo qui per altre persone come me :)
Michal B.

@MichalB. Non ti manca la sintassi interna? es: listagg (tipo, ',') all'interno del gruppo (ordina per nome)?
Gregory,

@gregory: ho modificato la mia risposta. Penso che la mia vecchia soluzione funzionasse in passato. L'attuale modulo che hai suggerito funzionerà sicuramente, grazie.
Michal B.

1
per la gente futura - puoi scrivere una nuova domanda con la tua risposta per una differenza significativa come una piattaforma diversa
Mike M

7

Questo tipo di domanda viene posta qui molto spesso e la soluzione dipenderà molto dai requisiti sottostanti:

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

e

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

In genere, non esiste un solo modo SQL per farlo senza sql dinamico, una funzione definita dall'utente o un cursore.


2
Non vero. La soluzione di cyberkiwi che utilizza cte: s è sql puro senza alcuna pirateria informatica specifica del fornitore.
Björn Lindqvist,

1
Al momento della domanda e della risposta, non avrei considerato i CTE ricorsivi terribilmente portatili, ma ora sono supportati da Oracle. La soluzione migliore dipenderà dalla piattaforma. Per SQL Server è molto probabilmente la tecnica FOR XML o un aggregato CLR del cliente.
Cade Roux,

1
la risposta definitiva per tutte le domande? stackoverflow.com/search?q=[qualunque sia la domanda]
Junchen Liu

7

Solo per aggiungere a ciò che ha detto Cade, questa è di solito una cosa di visualizzazione front-end e dovrebbe quindi essere gestita lì. So che a volte è più facile scrivere qualcosa al 100% in SQL per cose come l'esportazione di file o altre soluzioni "solo SQL", ma il più delle volte questa concatenazione dovrebbe essere gestita nel tuo livello di visualizzazione.


11
Il raggruppamento è una cosa di visualizzazione front-end ora? Esistono numerosi scenari validi per concatenare una colonna in un set di risultati raggruppati.
MGOwen,

5

Non è necessario un cursore ... un ciclo while è sufficiente.

------------------------------
-- 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


@marc_s forse una critica migliore è che PRIMARY KEY dovrebbe essere dichiarato sulle variabili della tabella.
Amy B

@marc_s A ulteriore ispezione, quell'articolo è un falso, come quasi tutte le discussioni sulle prestazioni senza misurazione IO. Ho imparato a conoscere il GAL - quindi grazie per quello.
Amy B

4

Diventiamo molto semplici:

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

Sostituisci questa riga:

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

Con la tua domanda.


3

non ho visto risposte incrociate, inoltre non è necessario estrarre xml. Ecco una versione leggermente diversa di ciò che ha scritto Kevin Fairchild. È più veloce e più facile da usare in query più complesse:

   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

1
Senza usare Value, possiamo incorrere in problemi in cui il testo è un carattere codificato XML
vCillusion

2

È possibile migliorare le prestazioni in modo significativo nel modo seguente se raggruppa per contiene principalmente un elemento:

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

Supponendo che non si vogliano nomi duplicati nell'elenco, che è possibile o meno.
jnm2,

1

Utilizzo della funzione Sostituisci e FOR JSON PATH

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

Per dati di esempio e altri modi clicca qui


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.