Manera óptima de concatenar / agregar cadenas

102

Estoy encontrando una manera de agregar cadenas de diferentes filas en una sola fila. Estoy buscando hacer esto en muchos lugares diferentes, por lo que sería bueno tener una función para facilitar esto. He probado soluciones usando COALESCEy FOR XML, pero simplemente no me sirven.

La agregación de cadenas haría algo como esto:

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

He echado un vistazo a las funciones agregadas definidas por CLR como reemplazo de COALESCEy FOR XML, pero aparentemente SQL Azure no es compatible con cosas definidas por CLR, lo cual es una molestia para mí porque sé que poder usarlo resolvería una gran cantidad de problemas para mi.

¿Existe alguna solución alternativa o un método igualmente óptimo (que podría no ser tan óptimo como CLR, pero bueno , tomaré lo que pueda obtener) que pueda usar para agregar mis cosas?

mate
fuente
¿De qué manera no for xmlte funciona?
Mikael Eriksson
4
Funciona, pero eché un vistazo al plan de ejecución y cada uno for xmlmuestra un uso del 25% en términos de rendimiento de la consulta (¡la mayor parte de la consulta!)
Matt
2
Hay diferentes formas de realizar la for xml pathconsulta. Algunos más rápidos que otros. Podría depender de sus datos, pero los que usan distinctson, en mi experiencia, más lentos que los que usan group by. Y si está utilizando .value('.', nvarchar(max))para obtener los valores concatenados, debe cambiar eso a.value('./text()[1]', nvarchar(max))
Mikael Eriksson
3
Su respuesta aceptada se parece a mi respuesta en stackoverflow.com/questions/11137075/… que pensé que es más rápido que XML. No se deje engañar por el costo de la consulta, necesita muchos datos para ver cuál es más rápido. XML es más rápido, que resulta ser la respuesta de @ MikaelEriksson a la misma pregunta . Opte por el enfoque XML
Michael Buen
2
Vote por una solución nativa para esto aquí: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Respuestas:

67

SOLUCIÓN

La definición de óptimo puede variar, pero aquí se explica cómo concatenar cadenas de diferentes filas mediante Transact SQL normal, que debería funcionar bien en 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

EXPLICACIÓN

El enfoque se reduce a tres pasos:

  1. Numere las filas usando OVERy PARTITIONagrupando y ordenándolas según sea necesario para la concatenación. El resultado es PartitionedCTE. Mantenemos recuentos de filas en cada partición para filtrar los resultados más tarde.

  2. Usando CTE recursivo ( Concatenated) iterar a través de los números de fila ( NameNumbercolumna) agregando Namevalores a la FullNamecolumna.

  3. Filtra todos los resultados excepto los que tengan los más altos NameNumber.

Tenga en cuenta que para que esta consulta sea predecible, uno debe definir tanto la agrupación (por ejemplo, en su escenario, las filas con lo mismo IDestán concatenados) como la ordenación (asumí que simplemente ordenó la cadena alfabéticamente antes de la concatenación).

Probé rápidamente la solución en SQL Server 2012 con los siguientes datos:

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

El resultado de la consulta:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
Serge Belov
fuente
5
Verifiqué el consumo de tiempo de esta manera con xmlpath y llegué a aproximadamente 4 milisegundos frente a aproximadamente 54 milisegundos. por lo que la forma xmplath es mejor especialmente en casos grandes. Escribiré el código de comparación en una respuesta separada.
QMaster
Es mucho mejor ya que este enfoque solo funciona para un máximo de 100 valores.
Romano Zumbé
@ romano-zumbé Utilice MAXRECURSION para establecer el límite de CTE a lo que necesite.
Serge Belov
1
Sorprendentemente, CTE fue mucho más lento para mí. sqlperformance.com/2014/08/t-sql-queries/… compara un montón de técnicas y parece estar de acuerdo con mis resultados.
Nickolay
Esta solución para una tabla con más de 1 millón de registros no funciona. Además, tenemos un límite en la profundidad recursiva
Ardalan Shahgholi
51

¿Son los métodos que usan FOR XML PATH como a continuación realmente tan lentos? Itzik Ben-Gan escribe que este método tiene un buen rendimiento en su libro T-SQL Querying (el Sr. Ben-Gan es una fuente confiable, en mi opinión).

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
fuente
No olvide poner un índice en esa idcolumna una vez que el tamaño de la tabla se convierta en un problema.
milivojeviCH
1
Y después de leer cómo funcionan las cosas / para la ruta xml ( stackoverflow.com/a/31212160/1026 ), estoy seguro de que es una buena solución a pesar de XML en su nombre :)
Nickolay
1
@slackterman Depende del número de registros en los que se operará. Creo que XML es deficiente en los recuentos bajos, en comparación con CTE, pero en los recuentos de volumen superior, alivia la limitación del Departamento de recursividad y es más fácil de navegar, si se hace de manera correcta y sucinta.
GoldBishop
¡Los métodos FOR XML PATH explotan si tienes emojis o caracteres especiales / sustitutos en tus datos!
devinbost
1
Este código da como resultado texto codificado en xml ( &cambiado a &, y así sucesivamente). Aquífor xml se proporciona una solución más correcta .
Frédéric
33

Para aquellos de nosotros que encontramos esto y no están usando Azure SQL Database:

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

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

(Gracias a @Brianjorden y @milanio por la actualización de Azure)

Código de ejemplo:

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

Violín SQL: http://sqlfiddle.com/#!18/89251/1

Hrobky
fuente
1
Lo acabo de probar y ahora funciona bien con Azure SQL Database.
Milanio
5
STRING_AGGse retrasó a 2017. No está disponible en 2016.
Morgan Thrapp
1
Gracias, Aamir y Morgan Thrapp por el cambio de versión de SQL Server. Actualizado. (En el momento de redactar este artículo, se afirmó que era compatible con la versión 2016.)
Hrobky
25

Aunque la respuesta de @serge es correcta, comparé el consumo de tiempo de su camino con xmlpath y descubrí que xmlpath es mucho más rápido. Escribiré el código de comparación y podrás comprobarlo tú mismo. Esta es la forma @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

Y esta es la forma 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
fuente
2
+1, tú QMaster (de las Artes Oscuras) tú! Obtuve una diferencia aún más dramática. (~ 3000 ms CTE frente a ~ 70 ms XML en SQL Server 2008 R2 en Windows Server 2008 R2 en Intel Xeon E5-2630 v4 @ 2.20 GHZ x2 con ~ 1 GB libre). Las únicas sugerencias son: 1) Utilice OP o (preferiblemente) términos genéricos para ambas versiones, 2) Dado que la Q de OP es cómo "concatenar / agregar cadenas " y esto solo es necesario para cadenas (frente a un valor numérico ), genérico los términos son demasiado genéricos. Simplemente use "GroupNumber" y "StringValue", 3) Declare y use una variable "Delimiter" y use "Len (Delimiter)" vs. "2".
Tom
1
+1 para no expandir el carácter especial a la codificación XML (por ejemplo, '&' no se expande a '& amp;' como en muchas otras soluciones inferiores)
Ingeniero
13

Actualización: MS SQL Server 2017+, Azure SQL Database

Se puede utilizar: STRING_AGG.

El uso es bastante simple para la solicitud de OP:

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

Lee mas

Bueno, mi anterior no respuesta se eliminó legítimamente (se dejó intacta a continuación), pero si alguien aterriza aquí en el futuro, hay buenas noticias. También han implementado STRING_AGG () en Azure SQL Database. Eso debería proporcionar la funcionalidad exacta originalmente solicitada en esta publicación con soporte nativo e integrado. @hrobky mencionó esto anteriormente como una característica de SQL Server 2016 en ese momento.

--- Publicación anterior: No hay suficiente reputación aquí para responder a @hrobky directamente, pero STRING_AGG se ve muy bien, sin embargo, solo está disponible en SQL Server 2016 vNext actualmente. Con suerte, también seguirá pronto a Azure SQL Datababse.

Brian Jorden
fuente
2
Yo sólo lo he probado y funciona como un encanto en Azure SQL Base de datos
Milanio
4
STRING_AGG()se afirma que estará disponible en SQL Server 2017, en cualquier nivel de compatibilidad. docs.microsoft.com/en-us/sql/t-sql/functions/...
un CVn
1
Si. STRING_AGG no está disponible en SQL Server 2016.
Magne
2

Puede usar + = para concatenar cadenas, por ejemplo:

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

si selecciona @test, le dará todos los nombres concatenados

jvc
fuente
Especifique el dialecto o la versión de SQL desde cuándo se admite.
Hrobky
Esto funciona en SQL Server 2012. Tenga en cuenta que se puede crear una lista separada por comas conselect @test += name + ', ' from names
Art Schmidt
4
Esto utiliza un comportamiento indefinido y no es seguro. Esto es especialmente probable que dé un resultado extraño / incorrecto si tiene un ORDER BYen su consulta. Debe utilizar una de las alternativas enumeradas.
Dannnno
1
Este tipo de consulta nunca fue un comportamiento definido, y en SQL Server 2019 encontramos que tiene un comportamiento incorrecto de manera más consistente que en versiones anteriores. No use este enfoque.
Matthew Rodatus
2

Encontré la respuesta de Serge muy prometedora, pero también encontré problemas de rendimiento con ella tal como estaba escrita. Sin embargo, cuando lo reestructuré para usar tablas temporales y no incluir tablas de CTE dobles, el rendimiento pasó de 1 minuto y 40 segundos a menos de un segundo para 1000 registros combinados. Aquí está para cualquiera que necesite hacer esto sin FOR XML en versiones anteriores de 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
fuente