Hola, chicos,
¡Buenas tardes!

En este post hablaré de una característica muy solicitada por Microsoft, que aún no ha creado de forma nativa en SQL Server, que es la concatenación de datos mediante agrupaciones, ya presente en otros DBMS como MySQL (GROUP_CONCAT), Oracle (XMLAGG) y PostgreeSQL (STRING_AGG o ARRAY_TO_STRING(ARRAY_AGG())).

Mucha gente piensa que esta característica es la función CONCAT(), introducida en SQL Server 2012, pero solo permite la concatenación de varias columnas de una fila en una sola columna y no la concatenación de varias filas en una sola columna.

Concatenar columnas en cadenas agrupadas consiste en transformar líneas en una cadena concatenada, de manera que se pueden agrupar los valores por algún campo, y se concatena la otra columna, como se muestra a continuación:

SQL Server - Grouped Concatenation convert rows into string
SQL Server: la concatenación agrupada convierte filas en cadenas

Para crear nuestra tabla de prueba, use los siguientes comandos:

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')
JUNTARSE

Cómo concatenar columnas en cadenas agrupadas usando 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: la concatenación agrupada convierte filas en cadenas - COALESCE

Como podemos ver arriba, usando la función COALESCE siempre necesitamos almacenar los resultados en una variable, y por esta razón no podemos obtener los resultados agrupados por categoría, como necesitamos. Si solo desea convertir columnas en una cadena, sin agruparlas, esta solución le conviene, pero no es la que necesitamos para esta publicación.

COSAS + PARA RUTA XML

Cómo concatenar columnas en cadenas agrupadas usando STUFF + FOR XML PATH

Una forma práctica y eficaz de resolver este problema es utilizar FOR XML PATH y STUFF para realizar una concatenación agrupada, de modo que el resultado sea exactamente el que esperábamos:

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: la concatenación agrupada convierte filas en cadenas - COSAS PARA LA RUTA XML

ACTUALIZAR

Cómo concatenar columnas en cadenas agrupadas usando ACTUALIZAR

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: la concatenación agrupada convierte filas en cadenas - ACTUALIZACIÓN

Otra solución que nos permitió generar datos según lo demandan nuestras necesidades.

BUCLE CON EL CURSOR

Cómo concatenar columnas en cadenas agrupadas usando LOOPING con 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: la concatenación agrupada convierte filas en cadenas - CURSOR

Esta solución también cumplió con lo que necesitábamos, pero como todos los bucles, no tiene buen rendimiento. Personalmente detesto la creación de cursores (a menos que TODAVÍA estés en SQL Server 2000), en el último caso, prefiero usar WHILE, pero tenemos mejores soluciones aquí en esta publicación.

BUCLE CON MIENTRAS

Cómo concatenar columnas en cadenas agrupadas usando LOOPING con 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: la concatenación agrupada convierte filas en cadenas - MIENTRAS

Se encontró la solución, pero como todos los bucles, no funciona.

CTE RECURSIVO

Cómo concatenar columnas en cadenas agrupadas usando 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: concatenación agrupada convierte filas en cadenas - CTE RECURSIVO

Esta solución utiliza la técnica de recursividad para resolver nuestro problema sin necesidad de realizar bucles. Aunque no es muy fácil de entender al principio, es una solución práctica y muy interesante.

FUNCIÓN ESCALAR (UDF)

Cómo concatenar columnas en cadenas agrupadas usando la función escalar (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: concatenación agrupada convierte filas en cadenas: FUNCIÓN ESCALAR UDF

Función muy útil y una vez creada, resulta muy práctica de utilizar. El problema de esta solución es que no tiene mucho rendimiento y requiere que crees una función para cada tabla que necesites para llevar a cabo esta solución, es decir, termina siendo poco genérica y necesitarías crear varias funciones que hagan prácticamente lo mismo en tu base de datos.

CLR de SQL (C#)

Cómo concatenar columnas en cadenas agrupadas usando 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: la concatenación agrupada convierte filas en cadenas - SQL CLR CSHARP

Mi solución favorita. Práctico, extremadamente rápido, genérico y resuelve nuestro problema. ¿No sabes qué es CLR o lo sabes, pero no sabes cómo implementarlo en tu base de datos? Entérate más visitando mi post Introducción a SQL CLR (Common Language Runtime) en SQL Server.

Consideraciones de rendimiento

Midamos ahora nuestras soluciones y descubramos cuáles son las más rápidas y cuáles las más lentas.

CLR

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - CLR

COSAS + PARA RUTA XML

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - COSAS PARA LA RUTA XML

ACTUALIZAR

SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - ACTUALIZACIÓN

CTE recursivo

SQL Server - Grouped Concatenation convert rows into string - Performance - CTE
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - CTE

función escalar UDF

SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function
SQL Server: concatenación agrupada convierte filas en cadenas - Rendimiento - Función UDF

Bucle mientras

SQL Server - Grouped Concatenation convert rows into string - Performance - WHILE
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - MIENTRAS

Bucle con cursor

SQL Server - Grouped Concatenation convert rows into string - Performance - CURSOR
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - CURSOR

Inmediatamente podemos eliminar los bucles, que eran mucho peores. ¿Qué pasa si insertamos más registros? Como 850.000 líneas... ¿Cómo será la actuación?

CLR

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR2
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - CLR2

COSAS + PARA RUTA XML

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - COSAS PARA LA RUTA XML

Como ningún otro método fue capaz de terminar el procesamiento en menos de 1 minuto, a pesar de haber usado 850.000 líneas con el CLR y FOR XML PATH, solo usaré 158.000 líneas (27%) para el resto de métodos para intentar analizar los resultados. Vamos a ver.

ACTUALIZAR

SQL Server - Grouped Concatenation convert rows into string - Performance - UPDATE
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - ACTUALIZACIÓN

El tiempo de ACTUALIZACIÓN no fue tan malo... 5,7 segundos para 158.000 líneas es un tiempo razonable.

función escalar UDF

SQL Server - Grouped Concatenation convert rows into string - Performance - UDF Function3
SQL Server: concatenación agrupada convierte filas en cadenas - Rendimiento - Función UDF3

CTE recursivo

SQL Server - Grouped Concatenation convert rows into string - Performance - CTE Recursive
SQL Server: concatenación agrupada convierte filas en cadenas - Rendimiento - CTE recursivo

Bueno, intenté esperar el CTE Recursivo... Incluso fui a almorzar y lo dejé corriendo aquí, pero después de 2 horas y 22 minutos no pude esperar más para terminar de procesar las 158 mil líneas (recordando que el CLR procesó 850.000 en 3s y el FOR XML en 1,5s).

Aparentemente, nuestro ganador fue FOR XML PATH, pero si analizamos los resultados, CLR entregó el resultado correcto, incluso con toda esta absurda cantidad de líneas:

SQL Server - Grouped Concatenation convert rows into string - Performance - CLR4
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - CLR4

En cuanto a FOR XML PATH… Terminó perdiéndose y los resultados no estaban en el orden correcto.

SQL Server - Grouped Concatenation convert rows into string - Performance - STUFF FOR XML PATH4
SQL Server: la concatenación agrupada convierte filas en cadenas - Rendimiento - COSAS PARA XML PATH4

Por tanto, la solución que recomiendo como la mejor de todas para este tipo de situaciones es SQL CLR!

¡Eso es todo, amigos!
Gracias por visitarnos y nos vemos en el próximo post.

función clr del servidor sql cosas para ruta xml cte recursivo convertir columnas filas agrupadas cadena de concatenación concat cómo convertir columnas concatenadas en cadena

función clr del servidor sql cosas para ruta xml cte recursivo convertir columnas filas agrupadas cadena de concatenación concat cómo convertir columnas concatenadas en cadena

Concatenar muchas filas en una sola cadena de texto

Concatenar muchas filas en una sola cadena de texto