Modo ottimale per concatenare / aggregare stringhe


102

Sto trovando un modo per aggregare stringhe di righe diverse in una singola riga. Sto cercando di farlo in molti posti diversi, quindi sarebbe bello avere una funzione per facilitare questo. Ho provato soluzioni usando COALESCEe FOR XML, ma semplicemente non mi hanno tagliato.

L'aggregazione di stringhe farebbe qualcosa del genere:

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

Ho dato un'occhiata alle funzioni aggregate definite da CLR in sostituzione di COALESCEe FOR XML, ma apparentemente SQL Azure non supporta le cose definite da CLR, il che è un problema per me perché so che essere in grado di usarlo risolverebbe un sacco di problemi per me.

Esiste una possibile soluzione alternativa o un metodo altrettanto ottimale (che potrebbe non essere ottimale come CLR, ma ehi , prenderò quello che posso ottenere) che posso usare per aggregare le mie cose?


In che modo for xmlnon funziona per te?
Mikael Eriksson

4
Funziona, ma ho dato un'occhiata al piano di esecuzione e ognuno for xmlmostra un utilizzo del 25% in termini di prestazioni della query (la maggior parte della query!)
Matt

2
Esistono diversi modi per eseguire la for xml pathquery. Alcuni più veloci di altri. Potrebbe dipendere dai tuoi dati, ma quelli che utilizzano distinctsono nella mia esperienza più lenti rispetto all'utilizzo group by. E se stai usando .value('.', nvarchar(max))per ottenere i valori concatenati dovresti cambiarlo in.value('./text()[1]', nvarchar(max))
Mikael Eriksson

3
La tua risposta accettata assomiglia alla mia risposta su stackoverflow.com/questions/11137075/… che ho pensato sia più veloce di XML. Non lasciarti ingannare dal costo delle query, hai bisogno di ampi dati per vedere quale è più veloce. XML è più veloce, che sembra essere la risposta di @ MikaelEriksson alla stessa domanda . Optare per l'approccio XML
Michael Buen

2
Si prega di votare per una soluzione nativa per questo qui: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Risposte:


67

SOLUZIONE

La definizione di ottimale può variare, ma ecco come concatenare stringhe da righe diverse usando Transact SQL normale, che dovrebbe funzionare correttamente in 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

SPIEGAZIONE

L'approccio si riduce a tre passaggi:

  1. Numerare le righe utilizzando OVERe PARTITIONraggruppandole e ordinandole secondo necessità per la concatenazione. Il risultato è PartitionedCTE. Manteniamo il conteggio delle righe in ogni partizione per filtrare i risultati in seguito.

  2. Utilizzando CTE ricorsivo ( Concatenated) iterare attraverso i numeri di riga ( NameNumbercolonna) aggiungendo Namevalori alla FullNamecolonna.

  3. Filtra tutti i risultati tranne quelli con il più alto NameNumber.

Tieni presente che per rendere prevedibile questa query è necessario definire sia il raggruppamento (ad esempio, nel tuo scenario le righe con le stesse IDsono concatenate) che l'ordinamento (presumo che tu semplicemente ordini la stringa alfabeticamente prima della concatenazione).

Ho testato rapidamente la soluzione su SQL Server 2012 con i seguenti dati:

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

Il risultato della query:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks

5
Ho controllato il consumo di tempo in questo modo rispetto a xmlpath e ho raggiunto circa 4 millisecondi contro circa 54 millisecondi. quindi il metodo xmplath è migliore specialmente nei casi di grandi dimensioni. Scriverò il codice di confronto in una risposta separata.
QMaster

È molto meglio poiché questo approccio funziona solo per un massimo di 100 valori.
Romano Zumbé

@ romano-zumbé Usa MAXRECURSION per impostare il limite CTE su tutto ciò di cui hai bisogno.
Serge Belov

1
Sorprendentemente, CTE è stato molto più lento per me. sqlperformance.com/2014/08/t-sql-queries/… confronta un sacco di tecniche e sembra concordare con i miei risultati.
Nickolay

Questa soluzione per una tabella con più di 1 milione di record non funziona. Inoltre, abbiamo un limite alla profondità ricorsiva
Ardalan Shahgholi

51

I metodi che utilizzano FOR XML PATH come di seguito sono davvero così lenti? Itzik Ben-Gan scrive che questo metodo ha buone prestazioni nel suo libro T-SQL Querying (Mr. Ben-Gan è una fonte affidabile, a mio avviso).

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

Non dimenticare di inserire un indice su quella idcolonna una volta che la dimensione di una tabella diventa un problema.
milivojeviCH

1
E dopo aver letto come funziona stuff / for xml path ( stackoverflow.com/a/31212160/1026 ), sono sicuro che sia una buona soluzione nonostante XML nel suo nome :)
Nickolay

1
@slackterman Dipende dal numero di record su cui operare. Penso che XML sia carente nei conteggi bassi, rispetto a CTE, ma ai conteggi di volume più alti, allevia la limitazione di Recursion Dept ed è più facile da navigare, se fatto correttamente e in modo succinto.
GoldBishop

I metodi FOR XML PATH saltano in aria se hai emoji o caratteri speciali / surrogati nei tuoi dati !!!
devinbost

1
Questo codice risulta in testo con codifica xml ( &passato a &e così via). Una for xmlsoluzione più corretta viene fornita qui .
Frédéric

33

Per quelli di noi che l'hanno trovato e non usano il database SQL di Azure:

STRING_AGG()in 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/ funzioni / string-agg-transact-sql

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

(Grazie a @Brianjorden e @milanio per l'aggiornamento di Azure)

Codice di esempio:

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

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


1
L'ho appena testato e ora funziona bene con il database SQL di Azure.
milanio

5
STRING_AGGè stato rinviato al 2017. Non è disponibile nel 2016.
Morgan Thrapp

1
Grazie, Aamir e Morgan Thrapp per il cambio di versione di SQL Server. Aggiornato. (Al momento della stesura di questo documento è stato affermato di essere supportato nella versione 2016.)
Hrobky

25

Sebbene la risposta di @serge sia corretta, ho confrontato il consumo di tempo con xmlpath e ho scoperto che xmlpath è molto più veloce. Scriverò il codice di confronto e potrai controllarlo da solo. Questo è il modo @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 questo è il modo 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

2
+1, tu QMaster (delle Arti Oscure) tu! Ho un diff ancora più drammatico. (~ 3000 msec CTE vs ~ 70 msec XML su SQL Server 2008 R2 su Windows Server 2008 R2 su Intel Xeon E5-2630 v4 a 2,20 GHZ x2 con ~ 1 GB libero). Solo i suggerimenti sono: 1) Utilizzare i termini OP o (preferibilmente) generici per entrambe le versioni, 2) Poiché Q. di OP è come "concatenare / aggregare stringhe " e questo è necessario solo per le stringhe (rispetto a un valore numerico ), generico i termini sono troppo generici. Basta usare "GroupNumber" e "StringValue", 3) Dichiarare e utilizzare una variabile "Delimiter" e utilizzare "Len (Delimiter)" rispetto a "2".
Tom

1
+1 per non espandere il carattere speciale alla codifica XML (ad esempio, "&" non viene espanso in "& amp;" come in tante altre soluzioni inferiori)
Ingegnere inverso

13

Aggiornamento: Ms SQL Server 2017+, database SQL di Azure

È possibile utilizzare: STRING_AGG.

L'utilizzo è piuttosto semplice per la richiesta di OP:

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

Leggi di più

Bene, la mia vecchia non risposta è stata giustamente cancellata (lasciata intatta sotto), ma se qualcuno dovesse atterrare qui in futuro, ci sono buone notizie. Hanno implementato STRING_AGG () anche nel database SQL di Azure. Ciò dovrebbe fornire la funzionalità esatta originariamente richiesta in questo post con supporto nativo e integrato. @hrobky lo ha menzionato in precedenza come funzionalità di SQL Server 2016 all'epoca.

--- Vecchio messaggio: reputazione insufficiente qui per rispondere direttamente a @hrobky, ma STRING_AGG ha un bell'aspetto, tuttavia al momento è disponibile solo in SQL Server 2016 vNext. Si spera che presto seguirà anche Azure SQL Datababse.


2
L'ho appena testato e funziona a
meraviglia

4
STRING_AGG()è dichiarato per diventare disponibile in SQL Server 2017, in qualsiasi livello di compatibilità. docs.microsoft.com/en-us/sql/t-sql/functions/…
un CVn

1
Sì. STRING_AGG non è disponibile in SQL Server 2016.
Magne

2

Puoi usare + = per concatenare le stringhe, ad esempio:

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

se selezioni @test, ti verranno forniti tutti i nomi concatenati


Si prega di specificare il dialetto SQL o la versione da quando è supportato.
Hrobky

Funziona in SQL Server 2012. Si noti che è possibile creare un elenco delimitato da virgole conselect @test += name + ', ' from names
Art Schmidt,

4
Questo utilizza un comportamento indefinito e non è sicuro. Questo è particolarmente probabile che fornisca un risultato strano / errato se hai un ORDER BYnella tua query. Dovresti usare una delle alternative elencate.
Dannnno

1
Questo tipo di query non è mai stato definito come comportamento e in SQL Server 2019 abbiamo riscontrato che il comportamento non corretto era più coerente rispetto alle versioni precedenti. Non utilizzare questo approccio.
Matthew Rodatus

2

Ho trovato la risposta di Serge molto promettente, ma ho anche riscontrato problemi di prestazioni con essa come scritta. Tuttavia, quando l'ho ristrutturato per utilizzare tabelle temporanee e non includere tabelle CTE doppie, le prestazioni sono passate da 1 minuto e 40 secondi a un secondo per 1000 record combinati. Qui è per chiunque abbia bisogno di farlo senza FOR XML su versioni precedenti di 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
;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.