Forma ideal de concatenar / agregar strings

102

Estou descobrindo uma maneira de agregar strings de linhas diferentes em uma única linha. Estou procurando fazer isso em muitos lugares diferentes, então ter uma função para facilitar isso seria bom. Eu tentei soluções usando COALESCEe FOR XML, mas eles simplesmente não funcionam para mim.

A agregação de strings faria algo assim:

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus

Eu dei uma olhada nas funções de agregação definidas pelo CLR como um substituto para COALESCEe FOR XML, mas aparentemente o SQL Azure não oferece suporte a coisas definidas pelo CLR, o que é uma dor para mim porque eu sei que poder usá-lo resolveria um monte de problemas para mim.

Existe alguma solução alternativa possível ou método igualmente ideal (que pode não ser tão ideal quanto CLR, mas hey , vou pegar o que puder) que eu possa usar para agregar minhas coisas?

mate
fonte
De que forma for xmlnão funciona para você?
Mikael Eriksson
4
Funciona, mas eu dei uma olhada no plano de execução e cada for xmlum mostra um uso de 25% em termos de desempenho de consulta (grande parte da consulta!)
matt
2
Existem diferentes maneiras de fazer a for xml pathconsulta. Alguns mais rápidos do que outros. Pode depender dos seus dados, mas os que usam distincté mais lento do que usar group by. E se você estiver usando .value('.', nvarchar(max))para obter os valores concatenados, você deve alterar isso para.value('./text()[1]', nvarchar(max))
Mikael Eriksson
3
Sua resposta aceita se assemelha à minha resposta em stackoverflow.com/questions/11137075/… que pensei ser mais rápido do que XML. Não se deixe enganar pelo custo da consulta, você precisa de muitos dados para ver qual é mais rápido. XML é mais rápido, o que é a resposta de @MikaelEriksson à mesma pergunta . Optar pela abordagem XML
Michael Buen
2
Vote

Respostas:

67

SOLUÇÃO

A definição de ideal pode variar, mas veja como concatenar strings de linhas diferentes usando Transact SQL regular, que deve funcionar bem no Azure.

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM dbo.SourceTable
),
Concatenated AS
(
    SELECT 
        ID, 
        CAST(Name AS nvarchar) AS FullName, 
        Name, 
        NameNumber, 
        NameCount 
    FROM Partitioned 
    WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, 
        CAST(C.FullName + ', ' + P.Name AS nvarchar), 
        P.Name, 
        P.NameNumber, 
        P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C 
                ON P.ID = C.ID 
                AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

EXPLICAÇÃO

A abordagem se resume em três etapas:

  1. Número as linhas usando OVERe PARTITIONde agrupamento e ordenando-os conforme necessário para a concatenação. O resultado é PartitionedCTE. Mantemos contagens de linhas em cada partição para filtrar os resultados posteriormente.

  2. Usando recursiva CTE ( Concatenated) iterar através dos números de linha ( NameNumbercoluna) adicionando Namevalores à FullNamecoluna.

  3. Filtre todos os resultados, exceto aqueles com o maior valor NameNumber.

Lembre-se de que, para tornar essa consulta previsível, é necessário definir o agrupamento (por exemplo, em seu cenário, as linhas com as mesmas IDsão concatenadas) e a classificação (presumi que você simplesmente classifique a string alfabeticamente antes da concatenação).

Testei rapidamente a solução no SQL Server 2012 com os seguintes dados:

INSERT dbo.SourceTable (ID, Name)
VALUES 
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')

O resultado da consulta:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
Serge Belov
fonte
5
Eu verifiquei o consumo de tempo dessa forma em relação ao xmlpath e alcancei cerca de 4 milissegundos versus cerca de 54 milissegundos. então o método xmplath é melhor especialmente em casos grandes. Vou escrever o código de comparação em uma resposta separada.
QMaster de
É muito melhor, pois essa abordagem só funciona para 100 valores no máximo.
Romano Zumbé
@ romano-zumbé Use MAXRECURSION para definir o limite CTE para tudo o que você precisa.
Serge Belov
1
Surpreendentemente, CTE foi muito mais lento para mim. sqlperformance.com/2014/08/t-sql-queries/… compara um monte de técnicas e parece concordar com meus resultados.
Nickolay
Esta solução para uma tabela com mais de 1 milhão de registros não funciona. Além disso, temos um limite de profundidade recursiva
Ardalan Shahgholi
51

Os métodos que usam FOR XML PATH como a seguir são realmente lentos? Itzik Ben-Gan escreve que esse método tem um bom desempenho em seu livro T-SQL Querying (o Sr. Ben-Gan é uma fonte confiável, na minha opinião).

create table #t (id int, name varchar(20))

insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')

select  id
        ,Names = stuff((select ', ' + name as [text()]
        from #t xt
        where xt.id = t.id
        for xml path('')), 1, 2, '')
from #t t
group by id
slachterman
fonte
Não se esqueça de colocar um índice nessa idcoluna assim que o tamanho da tabela se tornar um problema.
milivojeviCH
1
E depois de ler como as coisas / para o caminho xml funcionam ( stackoverflow.com/a/31212160/1026 ), tenho certeza de que é uma boa solução, apesar do XML em seu nome :)
Nickolay
1
@slackterman Depende do número de registros a serem operados. Acho que XML é deficiente nas contagens baixas, em comparação com o CTE, mas nas contagens de volume superiores, alivia a limitação do Departamento de Recursão e é mais fácil de navegar, se feito de forma correta e sucinta.
GoldBishop
Os métodos FOR XML PATH explodem se você tiver emojis ou caracteres especiais / substitutos em seus dados !!!
devinbost
1
Esse código resulta em texto codificado em xml ( &alternado para &e assim por diante). Uma for xmlsolução mais correta é fornecida aqui .
Frédéric
33

Para aqueles de nós que encontraram este e não estão usando o Banco de Dados SQL do Azure:

STRING_AGG()em PostgreSQL, SQL Server 2017 e Azure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.microsoft.com/en-us/sql/t-sql/ funções / string-agg-transact-sql

GROUP_CONCAT()no MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat

(Agradecimentos a @Brianjorden e @milanio pela atualização do Azure)

Código de exemplo:

select Id
, STRING_AGG(Name, ', ') Names 
from Demo
group by Id

SQL Fiddle: http://sqlfiddle.com/#!18/89251/1

Hrobky
fonte
1
Acabei de testá-lo e agora funciona bem com o Banco de Dados SQL do Azure.
milanio
5
STRING_AGGfoi adiado para 2017. Não está disponível em 2016.
Morgan Thrapp
1
Obrigado, Aamir e Morgan Thrapp pela mudança de versão do SQL Server. Atualizada. (No momento da redação, afirmava-se que era compatível com a versão 2016.)
Hrobky
25

Embora @serge a resposta esteja correta, comparei o consumo de tempo dele com o xmlpath e descobri que o xmlpath é muito mais rápido. Vou escrever o código de comparação e você pode verificar por si mesmo. Este é o jeito @serge:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (ID int, Name nvarchar(50))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE()

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM @YourTable
),
Concatenated AS
(
    SELECT ID, CAST(Name AS nvarchar) AS FullName, Name, NameNumber, NameCount FROM Partitioned WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, CAST(C.FullName + ', ' + P.Name AS nvarchar), P.Name, P.NameNumber, P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C ON P.ID = C.ID AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 54 milliseconds

E esta é a maneira xmlpath:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (@counter, ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE();

set nocount off
SELECT
    t1.HeaderValue
        ,STUFF(
                   (SELECT
                        ', ' + t2.ChildValue
                        FROM @YourTable t2
                        WHERE t1.HeaderValue=t2.HeaderValue
                        ORDER BY t2.ChildValue
                        FOR XML PATH(''), TYPE
                   ).value('.','varchar(max)')
                   ,1,2, ''
              ) AS ChildValues
    FROM @YourTable t1
    GROUP BY t1.HeaderValue

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 4 milliseconds
QMaster
fonte
2
+1, seu QMaster (das Artes das Trevas) você! Eu tenho uma diferença ainda mais dramática. (~ 3000 mseg CTE vs. ~ 70 mseg XML no SQL Server 2008 R2 no Windows Server 2008 R2 no Intel Xeon E5-2630 v4 @ 2,20 GHZ x2 com ~ 1 GB grátis). Apenas sugestões são: 1) Use OPs ou (de preferência) termos genéricos para ambas as versões, 2) Uma vez que OP's Q. é como "concatenar / agregar strings " e isso só é necessário para strings (vs. um valor numérico ), genérico termos são muito genéricos. Basta usar "GroupNumber" e "StringValue", 3) Declare e use uma variável "Delimiter" e use "Len (Delimiter)" vs. "2".
Tom
1
+1 para não expandir caracteres especiais para codificação XML (por exemplo, '&' não é expandido para '& amp;' como em tantas outras soluções inferiores)
Engenheiro
13

Atualização: Ms SQL Server 2017+, Banco de Dados SQL do Azure

Você pode usar: STRING_AGG.

O uso é muito simples para a solicitação do OP:

SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id

consulte Mais informação

Bem, minha antiga não resposta foi excluída por direito (deixada intacta abaixo), mas se acontecer de alguém pousar aqui no futuro, temos boas notícias. Eles implementaram STRING_AGG () no Banco de Dados SQL do Azure também. Isso deve fornecer a funcionalidade exata originalmente solicitada nesta postagem com suporte nativo e integrado. @hrobky mencionou isso anteriormente como um recurso do SQL Server 2016 na época.

--- Postagem antiga: Não há reputação suficiente aqui para responder a @hrobky diretamente, mas STRING_AGG parece ótimo, no entanto, está disponível apenas no SQL Server 2016 vNext atualmente. Esperançosamente, isso seguirá para o Azure SQL Datababse em breve.

Brian Jorden
fonte
2
Acabei de testar e funciona
perfeitamente no
4
STRING_AGG()está declarado para estar disponível no SQL Server 2017, em qualquer nível de compatibilidade. docs.microsoft.com/en-us/sql/t-sql/functions/…
um CV
1
Sim. STRING_AGG não está disponível no SQL Server 2016.
Magne
2

Você pode usar + = para concatenar strings, por exemplo:

declare @test nvarchar(max)
set @test = ''
select @test += name from names

se você selecionar @test, ele fornecerá todos os nomes concatenados

jvc
fonte
Especifique o dialeto SQL ou a versão desde quando é compatível.
Hrobky
Isso funciona no SQL Server 2012. Observe que uma lista separada por vírgulas pode ser criada comselect @test += name + ', ' from names
Art Schmidt
4
Isso usa um comportamento indefinido e não é seguro. É muito provável que isso dê um resultado estranho / incorreto se você tiver um ORDER BYem sua consulta. Você deve usar uma das alternativas listadas.
Dannnno
1
Este tipo de consulta nunca foi um comportamento definido, e no SQL Server 2019 descobrimos que ela apresentava o comportamento incorreto de forma mais consistente do que nas versões anteriores. Não use essa abordagem.
Matthew Rodatus
2

Achei a resposta de Serge muito promissora, mas também encontrei problemas de desempenho com a escrita. No entanto, quando eu o reestruturei para usar tabelas temporárias e não incluir tabelas CTE duplas, o desempenho foi de 1 minuto e 40 segundos para sub-segundos para 1000 registros combinados. Aqui, é para quem precisa fazer isso sem FOR XML em versões anteriores do SQL Server:

DECLARE @STRUCTURED_VALUES TABLE (
     ID                 INT
    ,VALUE              VARCHAR(MAX) NULL
    ,VALUENUMBER        BIGINT
    ,VALUECOUNT         INT
);

INSERT INTO @STRUCTURED_VALUES
SELECT   ID
        ,VALUE
        ,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY VALUE) AS VALUENUMBER
        ,COUNT(*) OVER (PARTITION BY ID)    AS VALUECOUNT
FROM    RAW_VALUES_TABLE;

WITH CTE AS (
    SELECT   SV.ID
            ,SV.VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    WHERE   VALUENUMBER = 1

    UNION ALL

    SELECT   SV.ID
            ,CTE.VALUE + ' ' + SV.VALUE AS VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    JOIN    CTE 
        ON  SV.ID = CTE.ID
        AND SV.VALUENUMBER = CTE.VALUENUMBER + 1

)
SELECT   ID
        ,VALUE
FROM    CTE
WHERE   VALUENUMBER = VALUECOUNT
ORDER BY ID
;
Tom Halladay
fonte