Оптимальный способ объединения / агрегирования строк


Я нахожу способ агрегировать строки из разных строк в одну строку. Я хочу сделать это во многих разных местах, поэтому иметь функцию для облегчения этого было бы неплохо. Я пробовал решения с помощью COALESCE и FOR XML, но они просто не сократить его для меня.

агрегация строк будет делать что-то вроде этого:

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

Я взглянул на CLR-определенные агрегатные функции как замена для COALESCE и FOR XML, но видимо SQL Azureне поддержка CLR-определенных вещей, что является болью для меня, потому что я знаю, что возможность использовать его решит для меня много проблем.

есть ли какой-либо возможный обходной путь или аналогичный оптимальный метод (который может быть не таким оптимальным, как CLR, но Эй я возьму то, что я могу получить), что я могу использовать для объединения моих вещей?

7 75

7 ответов:

решение

определение оптимальный может отличаться, но вот как объединить строки из разных строк с помощью обычного Transact SQL, который должен отлично работать в 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

объяснение

подход сводится к трем шагам:

  1. число строк с помощью OVER и PARTITION группировка и упорядочение их по мере необходимости для конкатенации. Этот результат Partitioned CTE. Мы сохраняем количество строк в каждой секции, чтобы фильтровать результаты позже.

  2. использование рекурсивного CTE (Concatenated) перебираем номера строк () добавлять Name значения .

  3. отфильтровать все результаты, но те, с самым высоким NameNumber.

пожалуйста, имейте в виду, что для того, чтобы сделать этот запрос предсказуемым, нужно определить обе группировки (для например, в вашем сценарии строки с тем же ID объединены) и сортировка (я предполагал, что вы просто сортируете строку в алфавитном порядке перед объединением).

я быстро протестировал решение на SQL Server 2012 со следующими данными:

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

результат запроса:

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

используются ли методы для XML-пути, как показано ниже, действительно так медленно? Ицик Бен-Ган пишет, что этот метод имеет хорошую производительность в своей книге запросов T-SQL (г-н Бен-Ган является надежным источником, на мой взгляд).

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

для тех из нас, кто нашел этот и не используют базу данных SQL Azure:

STRING_AGG() в PostgreSQL, SQL Server 2017 и 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

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

(спасибо @Brianjorden и @milanio за обновление Azure)

Пример Кода:

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

Скрипка SQL:http://sqlfiddle.com/#!18/89251/1

хотя @serge ответ правильный, но я сравнил потребление времени его пути против xmlpath, и я обнаружил, что xmlpath настолько быстрее. Я напишу код сравнения, и вы можете проверить его самостоятельно. Это @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

и это путь 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

обновление: Ms SQL Server 2017+, база данных SQL Azure

Вы можете использовать: STRING_AGG.

использование довольно просто для запроса OP:

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

Подробнее

Ну, мой старый не-ответ был по праву удален (оставленный в такте ниже), но если кто-то случайно приземлится здесь в будущем, есть хорошие новости. Они также имплиментировали STRING_AGG () в базе данных SQL Azure. Это должно обеспечить точное функциональность первоначально запрошена в этом посте с родной и встроенной поддержкой. @hrobky упоминал об этом ранее как о функции SQL Server 2016 в то время.

--- Старый Пост: Недостаточно репутации здесь, чтобы ответить на @hrobky напрямую, но STRING_AGG выглядит отлично, однако он доступен только в SQL Server 2016 vNext в настоящее время. Надеюсь, это будет следовать в Azure SQL Datababse в ближайшее время..

Я нашел ответ Сержа очень многообещающим, но я также столкнулся с проблемами производительности с ним, как написано. Однако, когда я изменил структуру, чтобы использовать временные таблицы и не включает в себя двойные таблицы КТР, спектакль прошел с 1 минуты 40 секунд в секунду для 1000 комбинированной записи. Вот это для тех, кто должен сделать это без for XML на более старых версиях 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
;

вы можете использовать += для конкатенации строк, например:

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

Если вы выберете @test, он даст вам все имена, связанные