Olá pessoal,
Boa tarde!

Neste post vou falar sobre um recurso bem requisitado para a Microsoft, e que ela ainda não criou nativamente no SQL Server, que é concatenação de dados utilizando agrupamentos, já presente em outros SGBDs como MySQL (GROUP_CONCAT), Oracle (XMLAGG) e PostgreeSQL (STRING_AGG ou ARRAY_TO_STRING(ARRAY_AGG())).

Muita gente acha que esse recurso é a função CONCAT(), introduzida no SQL Server 2012, mas ela permite apenas a concatenação de várias colunas de uma linha em uma coluna e não a concatenação de várias linhas em uma coluna.

A concatenação de colunas em strings agrupadas, consiste em transformar linhas em uma string concatenada, de forma que você possa agrupar os valores por algum campo, e a outra coluna seja concatenada, conforme exemplificado abaixo:

SQL Server - Grouped Concatenation convert rows into string
SQL Server - Grouped Concatenation convert rows into string

Para criar a nossa tabela de testes, utilize os comandos abaixo:

IF (OBJECT_ID('dbo.Teste_Group_Concat') IS NOT NULL) DROP TABLE dbo.Teste_Group_Concat
CREATE TABLE dbo.Teste_Group_Concat(
    Categoria VARCHAR(100),
    Descricao VARCHAR(100)
)

INSERT INTO dbo.Teste_Group_Concat( Categoria, Descricao )
VALUES ('Brinquedo', 'Bola'),
('Brinquedo', 'Carrinho'),
('Brinquedo', 'Boneco'),
('Brinquedo', 'Jogo'),
('Cama e Mesa', 'Toalha'),
('Cama e Mesa', 'Edredom'),
('Informatica', 'Teclado'),
('Informatica', 'Mouse'),
('Informatica', 'HD'),
('Informatica', 'CPU'),
('Informatica', 'Memoria'),
('Informatica', 'Placa-Mae'),
(NULL, 'TV')
COALESCE

Como concatenar colunas em strings agrupadas utilizando COALESCE

DECLARE @Nomes VARCHAR(MAX)

SELECT 
    @Nomes = COALESCE(@Nomes + ', ', '') + Descricao
FROM 
    dbo.Teste_XML

SELECT @Nomes

SQL Server - Grouped Concatenation convert rows into string - COALESCE
SQL Server - Grouped Concatenation convert rows into string - COALESCE

Como podemos observar acima, utilizando a função COALESCE, precisamos armazenar os resultados sempre em uma variável, e por este motivo, não conseguimos obter os resultados de forma agrupada por categoria, conforme precisamos. Se você quer apenas converter colunas em uma string, sem agrupar, essa solução irá te atender, mas não ao que precisamos para esse post.

STUFF + FOR XML PATH

Como concatenar colunas em strings agrupadas utilizando STUFF + FOR XML PATH

Uma forma prática e performática de se resolver esse problema, é utilizar o FOR XML PATH e o STUFF para realizarmos a concatenação agrupada, de forma que o resultado é exatamente aquilo que esperávamos:

SELECT  
    Categoria,
    STUFF((
        SELECT ', ' + B.Descricao 
        FROM dbo.Teste_Group_Concat B 
        WHERE ISNULL(B.Categoria, '') = ISNULL(A.Categoria, '')
        ORDER BY B.Descricao 
        FOR XML PATH('')), 1, 2, ''
    ) AS Descricao
FROM
    dbo.Teste_Group_Concat A
GROUP BY 
    Categoria
ORDER BY 
    Categoria

SQL Server - Grouped Concatenation convert rows into string - STUFF FOR XML PATH
SQL Server - Grouped Concatenation convert rows into string - STUFF FOR XML PATH

UPDATE

Como concatenar colunas em strings agrupadas utilizando UPDATE

DECLARE 
    @Categoria VARCHAR(MAX),
    @Descricoes VARCHAR(MAX)
 
DECLARE @Tabela TABLE (
    Categoria VARCHAR(200),
    Descricao VARCHAR(200),
    Descricoes VARCHAR(200)
)
 
INSERT @Tabela ( 
    Categoria,
    Descricao
)
SELECT  
    Categoria,
    Descricao
FROM
    dbo.Teste_Group_Concat
ORDER BY 
    Categoria,
    Descricao
 

UPDATE 
    @Tabela
SET
    @Descricoes = Descricoes = COALESCE(CASE COALESCE(@Categoria, '')
                                WHEN Categoria THEN @Descricoes + ', ' + Descricao
                                ELSE Descricao
                            END, ''),
    @Categoria = ISNULL(Categoria, '')


SELECT  
    Categoria,
    Descricoes = MAX(Descricoes)
FROM
    @Tabela
GROUP BY 
    Categoria
ORDER BY 
    Categoria

SQL Server - Grouped Concatenation convert rows into string - UPDATE
SQL Server - Grouped Concatenation convert rows into string - UPDATE

Mais uma solução que nos permitiu gerar os dados conforme nossa necessidade demanda.

LOOPING COM CURSOR

Como concatenar colunas em strings agrupadas utilizando LOOPING com CURSOR

DECLARE @Tabela TABLE (
    Categoria VARCHAR(MAX),
    Descricoes VARCHAR(MAX)
)
 
INSERT @Tabela ( 
    Categoria,
    Descricoes
)
SELECT  
    Categoria,
    ''
FROM
    dbo.Teste_Group_Concat
GROUP BY 
    Categoria
 

DECLARE 
    @Categoria VARCHAR(MAX),
    @Descricao VARCHAR(MAX),
    @Descricoes VARCHAR(MAX)
 
DECLARE c CURSOR LOCAL FAST_FORWARD
FOR

    SELECT  
        Categoria,
        Descricao
    FROM
        dbo.Teste_Group_Concat
    ORDER BY 
        Categoria,
        Descricao
 

OPEN c
 
FETCH c INTO @Categoria, @Descricao
 
WHILE @@FETCH_STATUS = 0
BEGIN

    UPDATE  @Tabela
    SET     Descricoes += ', ' + @Descricao
    WHERE   ISNULL(Categoria, '') = ISNULL(@Categoria, '')
 
    FETCH c INTO @Categoria, @Descricao
END
 
CLOSE c
DEALLOCATE c

SELECT  
    Categoria,
    Descricoes = STUFF(Descricoes, 1, 1, '')
FROM
    @Tabela
ORDER BY 
    Categoria

SQL Server - Grouped Concatenation convert rows into string - CURSOR
SQL Server - Grouped Concatenation convert rows into string - CURSOR

Essa solução também atendeu ao que precisamos, mas assim como todo looping, não é performático. Eu particularmente abomino a criação de cursores (a menos que você AINDA esteja no SQL Server 2000), em último caso, eu prefiro utilizar WHILE, mas temos soluções melhores aqui nesse post.

LOOPING COM WHILE

Como concatenar colunas em strings agrupadas utilizando LOOPING com WHILE

DECLARE @Tabela_Original TABLE (
    Id INT IDENTITY(1, 1),
    Categoria VARCHAR(MAX),
    Descricao VARCHAR(MAX)
)
INSERT @Tabela_Original ( 
    Categoria,
    Descricao
)
SELECT  
    Categoria,
    Descricao
FROM
    dbo.Teste_Group_Concat
ORDER BY
    Categoria


DECLARE @Tabela TABLE (
    Id INT IDENTITY(1, 1),
    Categoria VARCHAR(MAX),
    Descricoes VARCHAR(MAX)
)

INSERT @Tabela ( 
    Categoria,
    Descricoes
)
SELECT  
    Categoria,
    ''
FROM
    dbo.Teste_Group_Concat
GROUP BY 
    Categoria
 
DECLARE 
    @Categoria VARCHAR(MAX),
    @Descricao VARCHAR(MAX),
    @Descricoes VARCHAR(MAX),
    @Contador INT = 1,
    @Numero_Linhas INT = (SELECT COUNT(*) FROM @Tabela_Original)
 
 WHILE(@Contador <= @Numero_Linhas)
 BEGIN

    SELECT  
        @Categoria = ISNULL(Categoria, ''),
        @Descricao = Descricao
    FROM
        @Tabela_Original
    WHERE
        Id = @Contador
    ORDER BY 
        Categoria,
        Descricao
    

    UPDATE  @Tabela
    SET     Descricoes += ', ' + @Descricao
    WHERE   ISNULL(Categoria, '') = ISNULL(@Categoria, '')


    SET @Contador = @Contador + 1

END


SELECT  
    Categoria,
    Descricoes = STUFF(Descricoes, 1, 1, '')
FROM
    @Tabela
ORDER BY 
    Categoria

SQL Server - Grouped Concatenation convert rows into string - WHILE
SQL Server - Grouped Concatenation convert rows into string - WHILE

Solução atendida, mas como todo looping, não é performática.

CTE RECURSIVO

Como concatenar colunas em strings agrupadas utilizando CTE Recursivo

;WITH Dados as 
(
    SELECT 
        Categoria, 
        CONVERT(NVARCHAR(MAX), Descricao) AS Descricao,
        ROW_NUMBER() OVER (PARTITION BY Categoria ORDER BY Descricao) AS Linha
    FROM 
        dbo.Teste_Group_Concat
),
Tabela1 AS 
(
    SELECT Categoria, Descricao, Linha FROM Dados WHERE Linha = 1
),
Resultado AS
(
    SELECT Categoria, Descricao, Linha FROM Tabela1 WHERE Linha = 1
    
    UNION ALL

    SELECT Dados.Categoria, Resultado.Descricao + N', ' + Dados.Descricao, Dados.Linha
    FROM Dados 
    JOIN Resultado ON Resultado.Categoria = Dados.Categoria AND Dados.Linha = Resultado.Linha + 1
)
SELECT 
    Categoria, 
    Descricoes = MAX(Descricao)
FROM 
    Resultado
GROUP BY 
    Categoria 
ORDER BY 
    Categoria
OPTION (MAXRECURSION 0);

SQL Server - Grouped Concatenation convert rows into string - RECURSIVE CTE
SQL Server - Grouped Concatenation convert rows into string - RECURSIVE CTE

Essa solução utiliza da técnica de recursividade para resolver nosso problema sem precisar de looping. Embora não seja muito fácil de entender a princípio, é uma solução prática e muito interessante.

FUNÇÃO SCALAR (UDF)

Como concatenar colunas em strings agrupadas utilizando Função Scalar (UDF)

CREATE FUNCTION dbo.fncConcatCategoria ( 
    @Ds_Categoria VARCHAR(MAX) 
)
RETURNS VARCHAR(MAX)
WITH SCHEMABINDING
AS
BEGIN

    DECLARE @Retorno VARCHAR(MAX);
 
    SELECT  
        @Retorno = COALESCE(@Retorno + ', ', '') + Descricao
    FROM
        dbo.Teste_Group_Concat
    WHERE
        ISNULL(Categoria, '') = ISNULL(@Ds_Categoria, '')
    ORDER BY 
        Descricao
 
    RETURN @Retorno

END
GO

SELECT 
    Categoria,
    dbo.fncConcatCategoria(Categoria) AS Descricao
FROM 
    dbo.Teste_Group_Concat
GROUP BY
    Categoria

SQL Server - Grouped Concatenation convert rows into string - SCALAR FUNCTION UDF
SQL Server - Grouped Concatenation convert rows into string - SCALAR FUNCTION UDF

Função bem útil e após criada, se torna muito prática de utilizar. O problema dessa solução é que ela não é muito performática e exige que você crie uma função para cada tabela que você precise realizar essa solução, ou seja, acaba não sendo muito genérica e você precisaria criar várias funções que fazem praticamente a mesma coisa na sua base de dados.

SQL CLR (C#)

Como concatenar colunas em strings agrupadas utilizando SCL CLR (C#)

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text;

[Serializable]
[SqlUserDefinedAggregate(
    Format.UserDefined,
    IsInvariantToNulls = false,
    IsInvariantToDuplicates = false,
    IsInvariantToOrder = true,
    MaxByteSize = -1)]
public struct concat : IBinarySerialize, INullable
{
    private StringBuilder _accumulator;
    private string _delimiter;

    public bool IsNull { get; private set; }

    public void Init()
    {
        _accumulator = new StringBuilder();
        _delimiter = string.Empty;
        this.IsNull = true;
    }

    public void Accumulate([SqlFacet(MaxSize = -1)]SqlString Value, [SqlFacet(MaxSize = -1)]SqlString Delimiter)
    {

        if (Value.IsNull) return;

        if (!Delimiter.IsNull & Delimiter.Value.Length > 0)
        {
            _delimiter = Delimiter.Value;
            if (_accumulator.Length > 0)
                _accumulator.Append(Delimiter.Value);

        }

        _accumulator.Append(Value.Value);
        IsNull = false;
    }
    
    public void Merge(concat Group)
    {
        if (_accumulator.Length > 0
            & Group._accumulator.Length > 0) _accumulator.Append(_delimiter);

        _accumulator.Append(Group._accumulator.ToString());

    }

    public SqlString Terminate()
    {
        return new SqlString(_accumulator.ToString());
    }

    void IBinarySerialize.Read(System.IO.BinaryReader r)
    {
        _delimiter = r.ReadString();
        _accumulator = new StringBuilder(r.ReadString());

        if (_accumulator.Length != 0) this.IsNull = false;
    }

    void IBinarySerialize.Write(System.IO.BinaryWriter w)
    {
        w.Write(_delimiter);
        w.Write(_accumulator.ToString());
    }
}

SQL Server - Grouped Concatenation convert rows into string - SQL CLR CSHARP
SQL Server - Grouped Concatenation convert rows into string - SQL CLR CSHARP

Minha solução favorita. Prática, extremamente rápida, genérica e resolve nosso problema. Não sabe o que é CLR ou sabe, mas não sabe como implementar na sua base de dados? Saiba mais acessando meu post Introdução ao SQL CLR (Common Language Runtime) no SQL Server.

Considerações de Performance

Vamos medir agora as nossas soluções e descobrir quais são as mais rápidas e as mais lentas.

CLR

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR
SQL Server - Grouped Concatenation convert rows into string - Performance - CLR

STUFF + FOR XML PATH

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH
SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH

UPDATE

SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE
SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE

CTE Recursivo

SQL Server - Grouped Concatenation convert rows into string - Performance - CTE
SQL Server - Grouped Concatenation convert rows into string - Performance - CTE

Função scalar UDF

SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function
SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function

Loop While

SQL Server - Grouped Concatenation convert rows into string - Performance - WHILE
SQL Server - Grouped Concatenation convert rows into string - Performance - WHILE

Loop com Cursor

SQL Server - Grouped Concatenation convert rows into string - Performance - CURSOR
SQL Server - Grouped Concatenation convert rows into string - Performance - CURSOR

De cara já podemos eliminar os loopings, que foram muito piores.. O que acontece se a gente inserir mais registros? Tipo umas 850.000 linhas.. Como será a performance?

CLR

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR2
SQL Server - Grouped Concatenation convert rows into string - Performance - CLR2

STUFF + FOR XML PATH

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH
SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH

Como nenhum outro método conseguiu terminar de processar em menos de 1 minuto, apesar de ter usado 850.000 linhas com o CLR e o FOR XML PATH, vou utilizar apenas 158.000 linhas (27%) para o restante dos métodos pra tentar analisar os resultados.. Vamos ver.

UPDATE

SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE
SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE

Até que o tempo com o UPDATE não foi tão ruim.. 5.7s para 158.000 linhas é um tempo razoável.

Função scalar UDF

SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function3
SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function3

CTE Recursivo

SQL Server - Grouped Concatenation convert rows into string - Performance - CTE Recursive
SQL Server - Grouped Concatenation convert rows into string - Performance - CTE Recursive

Bom, tentei esperar pelo CTE Recursivo.. Até fui almoçar e deixei rodando aqui, mas depois de 2 horas e 22 minutos não deu pra esperar mais terminar de processar as 158 mil linhas (lembrando que o CLR processou 850.000 em 3s e o FOR XML em 1.5s)..

Aparentemente, nosso vencedor foi o FOR XML PATH, mas se formos analisar os resultados, o CLR entregou o resultado correto, mesmo com toda essa quantidade de linhas absurda:

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR4
SQL Server - Grouped Concatenation convert rows into string - Performance - CLR4

Já o FOR XML PATH… Acabou se perdendo e os resultados não ficaram na ordem correta.

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH4
SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH4

Sendo assim, a solução que eu indico como a melhor de todas para esse tipo de situação, é o SQL CLR!

É isso aí, pessoal!
Obrigado pela visita e até o próximo post.

sql server clr function stuff for xml path recursive cte convert columns rows grouped concat concatenation string como converter concatenar colunas em string

sql server clr function stuff for xml path recursive cte convert columns rows grouped concat concatenation string como converter concatenar colunas em string

Concatenate many rows into a single text string

Concatenate many rows into a single text string