Como usar o GROUP BY para concatenar seqüências de caracteres no SQL Server?

373

Como eu consigo:

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

para

id          Column
1          A:4, B:8
2          C:9
Eldila
fonte
18
Esse tipo de problema é resolvido facilmente no MySQL com sua GROUP_CONCAT()função agregada, mas resolvê-lo no Microsoft SQL Server é mais complicado. Consulte a seguinte pergunta do SO para obter ajuda: " Como obter vários registros em um registro com base na relação? "
Bill Karwin 7/07/08
11
Todos com uma conta da Microsoft deve votar em uma solução mais simples em conectar: connect.microsoft.com/SQLServer/feedback/details/427987/...
Jens Mühlenhoff
11
Você pode usar os agregados SQLCLR encontrada aqui como um substituto até que T-SQL é reforçada: groupconcat.codeplex.com
Orlando Colamatteo
11
Duplicado de stackoverflow.com/questions/194852/…
Salman A

Respostas:

550

Não é necessário nenhum CURSOR, WHILE loop ou Função Definida pelo Usuário .

Só precisa ser criativo com FOR XML e PATH.

[Nota: Esta solução funciona apenas no SQL 2005 e posterior. A pergunta original não especificou a versão em 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
Kevin Fairchild
fonte
6
por que alguém nolock uma tabela temporária?
Amy B
3
Esta é a coisa mais legal do SQL que eu já vi na minha vida. Alguma idéia se é "rápido" para grandes conjuntos de dados? Ele não começa a engatinhar como um cursor faria, não é? Eu gostaria que mais pessoas votassem nessa loucura.
user12861
6
Eh. Eu odeio o estilo de subconsulta dele. O JOINS é muito melhor. Só acho que não posso utilizar isso nesta solução. De qualquer forma, fico feliz em ver que há outros idiotas do SQL aqui, além de mim, que gostam de aprender coisas assim. Kudos a todos vocês :)
Kevin Fairchild
6
Uma maneira um pouco mais limpa de manipular as strings: STUFF ((SELECT ',' + [Name] + ':' + CAST ([Value] AS VARCHAR (MAX)) FROM #YourTable WHERE (ID = Results.ID) PARA XML PATH ( '')), 1,2, '') AS NameValues
Jonathan Sayce
3
Só para observar algo que eu encontrei. Mesmo em um ambiente que não diferencia maiúsculas de minúsculas, a parte .value da consulta PRECISA ser minúscula. Eu estou supondo que isto é porque XML-lo de que é sensível a maiúsculas
Jaloopa
136

Se for o SQL Server 2017 ou o SQL Server Vnext, SQL Azure, você poderá usar string_agg como abaixo:

select id, string_agg(concat(name, ':', [value]), ', ')
    from #YourTable 
    group by id
Kannan Kandasamy
fonte
Funciona perfeitamente!
Argoo 03/12/19
11
Isso funciona muito bem, melhor do que a resposta aceita.
Jannick Breunis
51

o uso do caminho XML não concatenará perfeitamente como seria de esperar ... ele substituirá "&" por "& amp;" e também vai mexer com <" and "> ... talvez algumas outras coisas, não tenho certeza ... mas você pode tentar isso

Me deparei com uma solução alternativa para isso ... você precisa substituir:

FOR XML PATH('')
)

com:

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

... ou NVARCHAR(MAX)se é isso que você está usando.

por que diabos não SQLtem uma função agregada concatenada? isso é uma PITA.

Allen
fonte
2
Eu vasculhei a rede procurando a melhor maneira de NÃO codificar a saída. Muito obrigado! Esta é a resposta definitiva - até que o MS adicione suporte adequado para isso, como uma função agregada CONCAT (). O que faço é lançar isso em um aplicativo externo que retorna meu campo concatenado. Não sou fã de adicionar seleções aninhadas em minhas instruções de seleção.
21713 MikeTeeVee
Concordei que, sem usar o Valor, podemos ter problemas em que o texto é um caractere codificado em XML. Por favor, encontre meu blog abordando cenários de concatenação agrupada no SQL server. blog.vcillusion.co.in/…
vCillusion
40

Corri para um par de problemas quando eu tentei converter a sugestão de Kevin Fairchild ao trabalho com cordas contendo espaços e caracteres XML especiais ( &, <, >) que foram codificadas.

A versão final do meu código (que não responde à pergunta original, mas pode ser útil para alguém) fica assim:

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

Em vez de usar um espaço como delimitador e substituir todos os espaços por vírgulas, ele apenas pré-vírgula e espaço para cada valor usado STUFFpara remover os dois primeiros caracteres.

A codificação XML é resolvida automaticamente usando a diretiva TYPE .

Jonathan Sayce
fonte
21

Outra opção usando o Sql Server 2005 e superior

---- 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
fonte
Obrigado pela contribuição, eu sempre prefiro usar CTEs e CTEs recursivos para resolver problemas no SQL server. Isso é trabalhado funciona para mim ótimo!
Gdavid
é possível usá-lo em uma consulta com aplicação externa?
fogo no buraco
14

Instale os agregados SQLCLR em http://groupconcat.codeplex.com

Em seguida, você pode escrever um código como esse para obter o resultado solicitado:

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
fonte
Eu a usei há alguns anos atrás, a sintaxe é muito mais limpa do que todos os truques "XML Path" e funciona muito bem. Eu recomendo fortemente quando as funções SQL CLR são uma opção.
AFract
12

O SQL Server 2005 e versões posteriores permitem que você crie suas próprias funções agregadas personalizadas , inclusive para concatenação - veja o exemplo na parte inferior do artigo vinculado.

Joel Coehoorn
fonte
4
Infelizmente, isso requer (?) O uso de assemblies CLR .. que é outro problema a ser tratado: - /
11
Apenas o exemplo usa o CLR para a implementação de concatenação real, mas isso não é necessário. Você pode fazer com que a função agregada de concatenação use FOR XML, pelo menos é melhor chamá-lo no futuro!
Shiv
12

Oito anos depois ... O Microsoft SQL Server vNext Database Engine finalmente aprimorou o Transact-SQL para suportar diretamente a concatenação de cadeias agrupadas. O Community Technical Preview versão 1.0 adicionou a função STRING_AGG e o CTP 1.1 adicionou a cláusula WITHIN GROUP para a função STRING_AGG.

Referência: https://msdn.microsoft.com/en-us/library/mt775028.aspx

Shem Sargent
fonte
9

Esta é apenas uma adição ao post de Kevin Fairchild (muito inteligente a propósito). Eu o teria adicionado como um comentário, mas ainda não tenho pontos suficientes :)

Eu estava usando essa idéia para uma visão em que estava trabalhando, no entanto, os itens que eu concatinava continha espaços. Então, modifiquei o código levemente para não usar espaços como delimitadores.

Mais uma vez obrigado pela solução legal 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
fonte
9

Um exemplo seria

No Oracle, você pode usar a função agregada LISTAGG.

Registros originais

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

Sql

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

Resulta em

name   type
------------
name1  type1
name2  type2; type3
Michal B.
fonte
6
Parece bom, mas as perguntas não são especificamente sobre a Oracle.
user12861
13
Compreendo. Mas eu estava procurando a mesma coisa para Oracle, então eu pensei que eu iria colocá-lo aqui para outras pessoas como eu :)
Michal B.
@MichalB. Você não está sentindo falta da sintaxe interna? por exemplo: listagg (tipo, ',') dentro do grupo (ordem pelo nome)?
Gregory
@gregory: editei minha resposta. Acho que minha solução antiga costumava funcionar nos dias de hoje. O formulário atual que você sugeriu funcionará com certeza, obrigado.
Michal B.
11
para pessoas futuras - você pode escrever uma nova pergunta com sua própria resposta para uma diferença significativa como plataforma diferente
Mike M
7

Esse tipo de pergunta é feita aqui com muita frequência e a solução dependerá muito dos requisitos subjacentes:

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

e

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

Normalmente, não há uma maneira somente SQL de fazer isso sem sql dinâmico, uma função definida pelo usuário ou um cursor.

Cade Roux
fonte
2
Não é verdade. A solução da cyberkiwi usando cte: s é sql puro, sem qualquer invasão específica do fornecedor.
Björn Lindqvist
11
No momento da pergunta e resposta, eu não consideraria CTEs recursivas como terrivelmente portáteis, mas agora elas são suportadas pela Oracle. A melhor solução vai depender da plataforma. Para o SQL Server, provavelmente é a técnica FOR XML ou um agregado CLR do cliente.
Cade Roux
11
a resposta final para todas as perguntas? stackoverflow.com/search?q=[ qualquer que seja a pergunta] #
Junchen Liu
7

Apenas para acrescentar ao que o Cade disse, isso geralmente é uma coisa de exibição de front-end e, portanto, deve ser tratado lá. Sei que às vezes é mais fácil escrever algo 100% no SQL para coisas como exportação de arquivos ou outras soluções "somente SQL", mas na maioria das vezes essa concatenação deve ser tratada na camada de exibição.

Tom H
fonte
11
Agrupar agora é uma coisa de exibição de front-end? Existem muitos cenários válidos para concatenar uma coluna em um conjunto de resultados agrupados.
MGOwen
5

Não precisa de um cursor ... um loop while é suficiente.

------------------------------
-- 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
fonte
@marc_s talvez uma crítica melhor seja que PRIMARY KEY deve ser declarada nas variáveis ​​da tabela.
Amy B
@marc_s Em uma inspeção mais aprofundada, esse artigo é uma farsa - como quase todas as discussões sobre desempenho sem medição de IO. Aprendi sobre o GAL - por isso, obrigado por isso.
Amy B
4

Vamos ser muito simples:

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

Substitua esta linha:

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

Com sua consulta.

Marquinho Peli
fonte
3

não encontrou nenhuma resposta cruzada, também não há necessidade de extração de xml. Aqui está uma versão ligeiramente diferente do que Kevin Fairchild escreveu. É mais rápido e fácil de usar em consultas mais complexas:

   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
fonte
11
Sem o uso de valor, podemos ter problemas onde o texto é um personagem XML codificado
vCillusion
2

Você pode melhorar o desempenho de maneira significativa da seguinte maneira, se agrupar por conter principalmente um item:

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
fonte
Supondo que você não queira nomes duplicados na lista, que você pode ou não.
jnm2
1

Usando Substituir Função 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

Para dados de amostra e mais maneiras, clique aqui

Mahesh
fonte
1

Se você tiver ativado o clr, poderá usar a biblioteca Group_Concat do GitHub

Manfred Wippel
fonte