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

104

Я нахожу способ объединить строки из разных строк в одну. Я хочу сделать это во многих разных местах, поэтому было бы неплохо иметь функцию для облегчения этого. Я пробовал решения с использованием 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, но эй , я возьму то , что я могу получить) , что я могу использовать , чтобы объединить свои вещи?

матовый
источник
Чем у вас for xmlне работает?
Микаэль Эрикссон,
4
Это действительно работает, но я взглянул на план выполнения, и каждый из них for xmlпоказывает 25% -ное использование с точки зрения производительности запросов (большая часть запроса!)
Мэтт
2
Есть разные способы выполнения for xml pathзапроса. Некоторые быстрее других. Это может зависеть от ваших данных, но те, которые используют distinct, по моему опыту, медленнее, чем использование group by. И если вы используете .value('.', nvarchar(max))для получения объединенных значений, вы должны изменить это на.value('./text()[1]', nvarchar(max))
Микаэль Эрикссон
3
Ваш принятый ответ напоминает мой ответ на stackoverflow.com/questions/11137075/… который, как я думал, быстрее, чем XML. Не обманывайтесь стоимостью запроса, вам нужно много данных, чтобы увидеть, что быстрее. XML работает быстрее, что является ответом @ MikaelEriksson на тот же вопрос . Выберите подход XML
Майкл Буэн,
2
Пожалуйста, проголосуйте за родное решение здесь: connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan

Ответы:

67

РЕШЕНИЕ

Определение оптимального может варьироваться, но вот как объединить строки из разных строк с помощью обычного 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группирования и упорядочения их по мере необходимости для конкатенации. Результат - PartitionedCTE. Мы ведем подсчет строк в каждом разделе, чтобы позже отфильтровать результаты.

  2. Используя рекурсивный CTE ( Concatenated), перебирайте номера строк ( NameNumberстолбцов), добавляя Nameзначения в FullNameстолбец.

  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
Серж Белов
источник
5
Я проверил потребление времени этим способом по xmlpath, и я достиг примерно 4 миллисекунд против примерно 54 миллисекунд. так что путь xmplath лучше особенно в больших случаях. Код сравнения напишу в отдельном ответе.
QMaster
Это намного лучше, поскольку этот подход работает максимум для 100 значений.
Romano Zumbé
@ romano-zumbé Используйте MAXRECURSION, чтобы установить предел CTE, который вам нужен.
Серж Белов
1
Удивительно, но CTE для меня был намного медленнее. sqlperformance.com/2014/08/t-sql-queries/… сравнивает кучу методов и, похоже, согласен с моими результатами.
Николай
Это решение для таблицы с более чем 1 миллионом записей не работает. Кроме того, у нас есть ограничение на рекурсивную глубину
Ардалан Шахголи
52

Действительно ли методы, использующие FOR XML PATH, как показано ниже, настолько медленны? Ицик Бен-Ган пишет, что этот метод имеет хорошую производительность в своей книге запросов 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
мясник
источник
Не забудьте поставить индекс на этот idстолбец, если размер таблицы станет проблемой.
milivojeviCH
2
И после прочтения того, как работают вещи / для пути xml ( stackoverflow.com/a/31212160/1026 ), я уверен, что это хорошее решение, несмотря на XML в его названии :)
Николай
1
@slackterman Зависит от количества записей, с которыми нужно работать. Я думаю, что XML неадекватен на низком уровне по сравнению с CTE, но при верхнем подсчете объема снимает ограничение отдела рекурсии и упрощает навигацию, если все сделано правильно и лаконично.
GoldBishop
FOR XML PATH методы взорвутся, если в ваших данных есть смайлики или специальные / суррогатные символы !!!
devinbost
1
Этот код приводит к тексту в кодировке xml ( &переключено на &и т. Д.). Более правильное for xmlрешение предоставляется здесь .
Фредерик
34

Для тех из нас, кто нашел это и не используют базу данных 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/ функции / строка-agg-transact-sql

GROUP_CONCAT()в 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 Fiddle: http://sqlfiddle.com/#!18/89251/1

Гробки
источник
1
Я только что протестировал его, и теперь он отлично работает с базой данных SQL Azure.
milanio
5
STRING_AGGбыл перенесен на 2017 год. Он недоступен в 2016 году.
Морган Трапп,
1
Спасибо, Аамир и Морган Трапп за изменение версии SQL Server. Обновлено. (На момент написания утверждалось, что он поддерживается в версии 2016.)
Hrobky
26

Хотя ответ @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
QMaster
источник
2
+1, ты, QMaster (Темных искусств) ты! Я получил еще более резкое различие. (~ 3000 мсек CTE против ~ 70 мсек XML на SQL Server 2008 R2 в Windows Server 2008 R2 на Intel Xeon E5-2630 v4 @ 2,20 ГГц x2 с ~ 1 ГБ свободного места). Только предложения: 1) Используйте OP или (предпочтительно) общие термины для обеих версий, 2) Поскольку OP Q. - это то, как «объединять / агрегировать строки », а это необходимо только для строк (вместо числового значения), общий термины слишком общие. Просто используйте «GroupNumber» и «StringValue». 3) Объявите и используйте переменную «Delimiter» и используйте «Len (Delimiter)» вместо «2».
Том
1
+1 за то, что специальный символ не расширяется в кодировку XML (например, '&' не расширяется до '& amp;', как во многих других неполноценных решениях)
Reversed Engineer
13

Обновление: 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.

Брайан Джорден
источник
2
Я только что протестировал его, и он
отлично
4
STRING_AGG()заявлено, что она станет доступной в SQL Server 2017 на любом уровне совместимости. docs.microsoft.com/en-us/sql/t-sql/functions/…
пользователь
1
Да. STRING_AGG недоступен в SQL Server 2016.
Магне,
2

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

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

если вы выберете @test, все имена будут объединены

jvc
источник
Укажите диалект или версию SQL, с каких пор он поддерживается.
Hrobky
Это работает в SQL Server 2012. Обратите внимание, что список, разделенный запятыми, можно создать с помощьюselect @test += name + ', ' from names
Art Schmidt
4
Это использует неопределенное поведение и небезопасно. Это особенно вероятно даст странный / неправильный результат, если у вас есть ORDER BYв вашем запросе. Вам следует использовать одну из перечисленных альтернатив.
Dannnno
1
Для этого типа запроса никогда не определялось поведение, и мы обнаружили, что в SQL Server 2019 неправильное поведение наблюдается более последовательно, чем в предыдущих версиях. Не используйте этот подход.
Мэтью Родатус,
2

Я нашел ответ Сержа очень многообещающим, но я также столкнулся с проблемами производительности, когда он был написан. Однако, когда я реструктурировал его, чтобы использовать временные таблицы и не включать двойные таблицы CTE, производительность упала с 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
;
Том Халладей
источник