Optimale Möglichkeit, Zeichenfolgen zu verketten / zu aggregieren

102

Ich finde eine Möglichkeit, Zeichenfolgen aus verschiedenen Zeilen in einer einzigen Zeile zusammenzufassen. Ich möchte dies an vielen verschiedenen Orten tun, daher wäre es schön, eine Funktion zu haben, die dies erleichtert. Ich habe Lösungen mit COALESCEund ausprobiert FOR XML, aber sie schneiden es einfach nicht für mich.

Die String-Aggregation würde ungefähr so ​​aussehen:

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

Ich habe mir CLR-definierte Aggregatfunktionen als Ersatz für COALESCEund angesehen FOR XML, aber anscheinend unterstützt SQL Azure keine CLR-definierten Funktionen, was für mich ein Schmerz ist, da ich weiß, dass die Verwendung eine ganze Menge davon lösen würde Probleme für mich.

Gibt es eine mögliche Abhilfe, oder in ähnlicher Weise eine optimale Verfahren (die als optimal nicht als CLR sein könnte, aber hey ich nehme , was ich kriegen kann) , dass ich verwenden kann , meine Sachen zu aggregieren?

matt
quelle
Inwiefern for xmlfunktioniert das bei Ihnen nicht?
Mikael Eriksson
4
Es funktioniert, aber ich habe mir den Ausführungsplan angesehen und jeder for xmlzeigt eine 25% ige Auslastung in Bezug auf die Abfrageleistung (ein Großteil der Abfrage!)
Matt
2
Es gibt verschiedene Möglichkeiten, die for xml pathAbfrage durchzuführen. Einige schneller als andere. Es könnte von Ihren Daten abhängen, aber die verwendeten distinctsind meiner Erfahrung nach langsamer als die group by. Und wenn Sie verwenden .value('.', nvarchar(max)), um die verketteten Werte zu erhalten, sollten Sie dies in.value('./text()[1]', nvarchar(max))
Mikael Eriksson
3
Ihre akzeptierte Antwort ähnelt meiner Antwort auf stackoverflow.com/questions/11137075/…, die meiner Meinung nach schneller als XML ist. Lassen Sie sich nicht von den Abfragekosten täuschen, Sie benötigen ausreichend Daten, um zu sehen, welche schneller sind. XML ist schneller, was zufällig die Antwort von @ MikaelEriksson auf dieselbe Frage ist . Entscheiden Sie sich für XML-Ansatz
Michael Buen
2
Bitte stimmen Sie hier für eine native Lösung ab: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Antworten:

67

LÖSUNG

Die Definition von optimal kann variieren. Hier erfahren Sie jedoch, wie Sie Zeichenfolgen aus verschiedenen Zeilen mithilfe von regulärem Transact SQL verketten, was in Azure problemlos funktionieren sollte.

;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

ERLÄUTERUNG

Der Ansatz besteht aus drei Schritten:

  1. Nummerieren Sie die Zeilen mit OVERund PARTITIONgruppieren und ordnen Sie sie nach Bedarf für die Verkettung. Das Ergebnis ist PartitionedCTE. Wir behalten die Anzahl der Zeilen in jeder Partition bei, um die Ergebnisse später zu filtern.

  2. Verwenden Sie rekursives CTE ( Concatenated), um die Zeilennummern ( NameNumberSpalte) zu durchlaufen und der Spalte NameWerte hinzuzufügen FullName.

  3. Filtern Sie alle Ergebnisse außer denen mit den höchsten heraus NameNumber.

Bitte beachten Sie, dass Sie, um diese Abfrage vorhersehbar zu machen, sowohl die Gruppierung (z. B. in Ihrem Szenario werden Zeilen mit derselben IDverkettet) als auch die Sortierung definieren müssen (ich habe angenommen, dass Sie die Zeichenfolge vor der Verkettung einfach alphabetisch sortieren).

Ich habe die Lösung unter SQL Server 2012 schnell mit den folgenden Daten getestet:

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

Das Abfrageergebnis:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
Serge Belov
quelle
5
Ich habe den Zeitverbrauch auf diese Weise mit xmlpath verglichen und ungefähr 4 Millisekunden gegenüber ungefähr 54 Millisekunden erreicht. Daher ist der xmplath-Weg besonders in großen Fällen besser. Ich werde den Vergleichscode in einer separaten Antwort schreiben.
QMaster
Es ist weitaus besser, da dieser Ansatz nur für maximal 100 Werte funktioniert.
Romano Zumbé
@ romano-zumbé Verwenden Sie MAXRECURSION, um das CTE-Limit auf das zu setzen, was Sie benötigen.
Serge Belov
1
Überraschenderweise war CTE für mich viel langsamer. sqlperformance.com/2014/08/t-sql-queries/… vergleicht eine Reihe von Techniken und scheint mit meinen Ergebnissen übereinzustimmen .
Nickolay
Diese Lösung für eine Tabelle mit mehr als 1 Million Datensätzen funktioniert nicht. Wir haben auch eine Grenze für die rekursive Tiefe
Ardalan Shahgholi
51

Sind Methoden, die FOR XML PATH wie unten verwenden, wirklich so langsam? Itzik Ben-Gan schreibt, dass diese Methode in seinem T-SQL-Abfragebuch eine gute Leistung aufweist (Herr Ben-Gan ist meiner Ansicht nach eine vertrauenswürdige Quelle).

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
quelle
Vergessen Sie nicht, einen Index für diese idSpalte zu erstellen , sobald die Größe einer Tabelle zum Problem wird.
MilivojeviCH
1
Und nachdem ich gelesen habe, wie stuff / for xml path funktioniert ( stackoverflow.com/a/31212160/1026 ), bin ich zuversichtlich, dass es trotz XML in seinem Namen eine gute Lösung ist :)
Nickolay
1
@slackterman Hängt von der Anzahl der Datensätze ab, die bearbeitet werden sollen. Ich denke, XML ist bei den niedrigen Zählungen im Vergleich zu CTE mangelhaft, aber bei den oberen Volumenzahlen verringert es die Einschränkung der Rekursionsabteilung und ist einfacher zu navigieren, wenn es richtig und prägnant ausgeführt wird.
GoldBishop
FOR XML PATH-Methoden werden angezeigt, wenn Ihre Daten Emojis oder Sonder- / Ersatzzeichen enthalten !!!
Devinbost
1
Dieser Code führt zu XML-codiertem Text ( &umgeschaltet auf &usw.). Eine korrektere for xmlLösung finden Sie hier .
Frédéric
33

Für diejenigen von uns, die dies gefunden haben und verwenden keine Azure SQL-Datenbank::

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

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

(Danke an @Brianjorden und @milanio für das Azure-Update)

Beispielcode:

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

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

Hrobky
quelle
1
Ich habe es gerade getestet und jetzt funktioniert es gut mit der Azure SQL-Datenbank.
Milanio
5
STRING_AGGwurde auf 2017 zurückgeschoben. Es ist nicht verfügbar in 2016.
Morgan Thrapp
1
Vielen Dank, Aamir und Morgan Thrapp, für die Änderung der SQL Server-Version. Aktualisiert. (Zum Zeitpunkt des Schreibens wurde behauptet, dass es in Version 2016 unterstützt wird.)
Hrobky
25

Obwohl die Antwort von @serge korrekt ist, habe ich den Zeitverbrauch seines Weges mit xmlpath verglichen und festgestellt, dass der xmlpath so schneller ist. Ich werde den Vergleichscode schreiben und Sie können ihn selbst überprüfen. Dies ist @serge Weg:

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

Und das ist xmlpath Weg:

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
quelle
2
+1, du QMaster (der dunklen Künste) du! Ich habe einen noch dramatischeren Unterschied. (~ 3000 ms CTE vs. ~ 70 ms XML unter SQL Server 2008 R2 unter Windows Server 2008 R2 unter Intel Xeon E5-2630 v4 bei 2,20 GHz x2 mit ~ 1 GB frei). Die einzigen Vorschläge sind: 1) Verwenden Sie entweder OPs oder (vorzugsweise) generische Begriffe für beide Versionen. 2) Da OPs Q. das "Verketten / Aggregieren von Strings " ist und dies nur für generische Strings (im Vergleich zu einem numerischen Wert) erforderlich ist Begriffe sind zu allgemein. Verwenden Sie einfach "GroupNumber" und "StringValue". 3) Deklarieren und verwenden Sie eine "Delimiter" -Variable und verwenden Sie "Len (Delimiter)" vs. "2".
Tom
1
+1 für das Nichterweiterung von Sonderzeichen auf XML-Codierung (z. B. '&' wird nicht wie bei so vielen anderen minderwertigen Lösungen auf '& amp;' erweitert)
Reversed Engineer
13

Update: Frau SQL Server 2017+, Azure SQL-Datenbank

Sie können verwenden : STRING_AGG.

Die Verwendung ist für die Anfrage von OP ziemlich einfach:

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

Weiterlesen

Nun, meine alte Nichtantwort wurde zu Recht gelöscht (unten in Kontakt gelassen), aber wenn jemand in Zukunft hier landet, gibt es gute Nachrichten. Sie haben STRING_AGG () auch in die Azure SQL-Datenbank implementiert. Dies sollte die genaue Funktionalität bieten, die ursprünglich in diesem Beitrag angefordert wurde, und native und integrierte Unterstützung bieten. @hrobky erwähnte dies zu diesem Zeitpunkt bereits als SQL Server 2016-Funktion.

--- Alter Beitrag: Nicht genug Ruf hier, um direkt auf @hrobky zu antworten, aber STRING_AGG sieht gut aus, ist jedoch derzeit nur in SQL Server 2016 vNext verfügbar. Hoffentlich folgt bald auch Azure SQL Datababse.

Brian Jorden
quelle
2
Ich habe es gerade getestet und es funktioniert wie ein Zauber in der Azure SQL-Datenbank
milanio
4
STRING_AGG()soll in SQL Server 2017 in jeder Kompatibilitätsstufe verfügbar sein. docs.microsoft.com/en-us/sql/t-sql/functions/…
ein CVn
1
Ja. STRING_AGG ist nicht in SQL Server 2016 verfügbar.
Magne
2

Sie können + = verwenden, um Zeichenfolgen zu verketten, zum Beispiel:

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

Wenn Sie @test auswählen, werden alle Namen verkettet

jvc
quelle
Bitte geben Sie den SQL-Dialekt oder die SQL-Version an, seit wann diese unterstützt wird.
Hrobky
Dies funktioniert in SQL Server 2012. Beachten Sie, dass eine durch Kommas getrennte Liste mitselect @test += name + ', ' from names
Art Schmidt
4
Dies verwendet undefiniertes Verhalten und ist nicht sicher. Dies führt besonders wahrscheinlich zu einem seltsamen / falschen Ergebnis, wenn Sie ein ORDER BYin Ihrer Abfrage haben. Sie sollten eine der aufgeführten Alternativen verwenden.
Dannnno
1
Diese Art von Abfrage wurde nie als Verhalten definiert, und in SQL Server 2019 haben wir festgestellt, dass das falsche Verhalten konsistenter ist als in früheren Versionen. Verwenden Sie diesen Ansatz nicht.
Matthew Rodatus
2

Ich fand die Antwort von Serge sehr vielversprechend, stieß aber auch auf Leistungsprobleme, wie geschrieben. Als ich es jedoch umstrukturierte, um temporäre Tabellen zu verwenden und keine doppelten CTE-Tabellen einzuschließen, stieg die Leistung für 1000 kombinierte Datensätze von 1 Minute 40 Sekunden auf unter Sekunden. Hier ist es für alle, die dies ohne FOR XML in älteren Versionen von SQL Server tun müssen:

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
quelle