Optymalny sposób łączenia / agregowania strun

Znajduję sposób na łączenie łańcuchów z różnych wierszy w jeden wiersz. Szukam tego w wielu różnych miejscach, więc posiadanie funkcji ułatwiającej to byłoby miłe. Próbowałem rozwiązań przy użyciu COALESCE i FOR XML, ale po prostu nie wyciąć go dla mnie.

Agregacja łańcuchów zrobiłaby coś takiego:

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus
Na przykład, jeśli nie masz dostępu do SQL Azure, możesz skorzystać z funkcji SQL Azure, które mogą być używane w aplikacjach SQL Azure. czy Nie wspierają rzeczy zdefiniowane przez CLR, co jest dla mnie bolesne, ponieważ Wiem, że możliwość ich użycia rozwiązałaby wiele problemów dla mnie.

Czy jest jakieś możliwe obejście, lub podobnie optymalna metoda (która może nie być tak optymalna jak CLR, ale Hej wezmę to, co mogę dostać), którą mogę wykorzystać do agregacji moich rzeczy?

Author: John, 2012-11-30

7 answers

Rozwiązanie

Definicja optimal może się różnić, ale oto jak łączyć Ciągi z różnych wierszy za pomocą zwykłego Transact SQL, co powinno działać dobrze na platformie 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

Wyjaśnienie

Podejście sprowadza się do trzech kroków:

  1. Numeruj wiersze za pomocą grupowania OVER i PARTITION i porządkuj je w zależności od potrzeb do konkatenacji. Wynikiem jest Partitioned CTE. Zachowujemy liczbę wierszy w każdej partycji do filtruj wyniki później.

  2. Używając rekurencyjnego CTE (Concatenated) iteratuj przez liczby wierszy (NameNumber kolumna) dodając Name wartości do FullName kolumna.

  3. Filtruj wszystkie wyniki, ale te z najwyższymi NameNumber.

Należy pamiętać, że aby to zapytanie było przewidywalne, należy zdefiniować zarówno grupowanie (na przykład w scenariuszu wiersze z tym samym ID są konkatenowane), jak i sortowanie (założyłem, że wystarczy posortować łańcuch Alfabetycznie przed konkatenacją).

Szybko przetestowałem rozwiązanie na SQL Server 2012 z następującymi danymi:

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

Wynik zapytania:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
 56
Author: Serge Belov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-04 19:24:28

Czy metody wykorzystujące ścieżkę XML, jak poniżej, są naprawdę tak powolne? Itzik Ben-Gan pisze, że ta metoda ma dobrą wydajność w jego książce zapytań T-SQL(Pan Ben-Gan jest wiarygodnym źródłem, moim zdaniem).

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
 40
Author: slachterman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-08 22:59:32

Dla tych z nas, którzy znaleźli to i nie używają Azure SQL Database :

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

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

(dzięki @ Brianjorden i @milanio for Azure update)

Przykładowy Kod:

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

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

 25
Author: Hrobky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-16 13:51:59

Chociaż @ serge odpowiedź jest poprawna, ale porównałem zużycie czasu jego drogi z xmlpath i stwierdziłem, że xmlpath jest tak szybszy. Napiszę kod porównawczy, a ty sam go sprawdzisz. To jest @ serge way:

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

I to jest xmlpath sposób:

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
 20
Author: QMaster,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-02 10:17:26

Aktualizacja: Ms SQL Server 2017+, Azure SQL Database

Możesz użyć: STRING_AGG.

Użycie jest dość proste dla żądania OP:

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

Czytaj Więcej

Cóż, mój stary Brak odpowiedzi został słusznie usunięty (zostawiony poniżej), ale jeśli ktoś zdarzy się tu wylądować w przyszłości, jest dobra wiadomość. Zaimplementowano również metodę STRING_AGG () w bazie danych Azure SQL. Które powinny zapewnić dokładną funkcjonalność pierwotnie żądaną w tym poście z natywne i wbudowane wsparcie. @hrobky wspomniał o tym wcześniej jako o funkcji SQL Server 2016 w tym czasie.

--- Stary Post: Nie ma tu wystarczającej reputacji, aby odpowiedzieć bezpośrednio na @hrobky, ale STRING_AGG wygląda świetnie, jednak jest obecnie dostępny tylko w SQL Server 2016 vNext. Mam nadzieję, że wkrótce pojawi się również na platformie Azure SQL Datababse..

 2
Author: Brian Jorden,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-18 09:35:49

Uznałem odpowiedź Serge ' a za bardzo obiecującą, ale napotkałem również problemy z wydajnością w formie pisemnej. Jednak, kiedy zrestrukturyzowałem go, aby używać tabel tymczasowych i nie zawierać podwójnych tabel CTE, wydajność poszła z 1 minuty 40 sekund do sub-sekundy dla 1000 połączonych rekordów. Tutaj jest dla każdego, kto musi to zrobić bez dla XML na starszych wersjach 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
;
 1
Author: Tom Halladay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-10 05:44:59

Możesz użyć + = do łączenia łańcuchów, na przykład:

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

Jeśli wybierzesz @test, to wszystkie nazwy zostaną skonkatenowane

 0
Author: jvc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-09 08:08:45