Оптимальний спосіб об'єднання / об'єднання рядків


102

Я знаходжу спосіб об’єднати рядки з різних рядків в один ряд. Я хочу зробити це в багатьох місцях, тому добре функціонувати для полегшення цього. Я спробував з допомогою рішення 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 мільйон записів не працює. Також у нас є обмеження щодо рекурсивної глибини
Ардалан Шахголі

51

Чи справді методи, що використовують 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

1
І прочитавши, як працюють речі / для шляху XML ( stackoverflow.com/a/31212160/1026 ), я впевнений, що це хороше рішення, незважаючи на назву XML у своєму імені :)
Nickolay

1
@slackterman Залежить від кількості записів, якими слід працювати. Я думаю, що XML має дефіцит при низьких показниках порівняно з CTE, але при верхніх підрахунках гучності зменшує обмеження рекурсійного відділу і легше орієнтуватися, якщо це зроблено правильно та стисло.
GoldBishop

ДЛЯ XML PATH методи вибухнуть, якщо у вас є емоції або спеціальні / сурогатні символи !!!
devinbost

1
Цей код призводить до закодованого xml тексту ( &переключений на &тощо). Більш правильне for xmlрішення надається тут .
Фредерік

33

Для тих із нас, хто це знайшов і не використовують бази даних Azure SQL:

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/ функції / string-agg-transact-sql

GROUP_CONCAT()в MySQL
http://dev.mysql.com/doc/refman/5.7/uk/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
Я щойно перевірив це, і тепер він працює чудово з базами даних Azure SQL.
milanio

5
STRING_AGGйого відсунули до 2017 року. Це недоступно у 2016 році.
Морган Трапп

1
Дякую, Аамір та Морган Трапп за зміну версії SQL Server. Оновлено. (На момент написання його заявляли, що його підтримують у версії 2016.)
Хробкі

25

Хоча відповідь @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

2
+1, ти QMaster (з темних мистецтв) ти! Я отримав ще більш драматичну різницю. (~ 3000 мс CTE проти ~ 70 мсек XML на SQL Server 2008 R2 на Windows Server 2008 R2 на Intel Xeon E5-2630 v4 при 2,20 ГГц x2 w / ~ 1 ГБ безкоштовно). Лише пропозиції: 1) Використовуйте загальні терміни OP або (бажано) загальні терміни для обох версій; 2) Оскільки Q ОП - це як "об'єднати / об'єднати рядки ", і це потрібно лише для рядків (проти числового значення), загальних терміни занадто загальні. Просто використовуйте "GroupNumber" та "StringValue", 3) Декларуйте та використовуйте змінну "Delimiter" і використовуйте "Len (Delimiter)" проти "2".
Том

1
+1 за нерозширення спеціального символу до кодування XML (наприклад, "&" не розширюється до "& amp;", як у багатьох інших неповноцінних рішеннях)
Engineer

13

Оновлення: пані SQL Server 2017+, база даних Azure SQL

Ви можете використовувати: STRING_AGG.

Використання для запиту ОП досить просто:

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

Детальніше

Ну мою стару невідповідь було правильно видалено (ліворуч в такті внизу), але якщо у майбутньому хтось приземлиться тут, є хороші новини. Вони також поширювали STRING_AGG () в Azure SQL Database. Це повинно забезпечувати точну функціональність, яку спочатку запитували у цій публікації, із вбудованою підтримкою. @hrobky раніше згадував про це як функцію SQL Server 2016.

--- Стара публікація: Тут недостатньо репутації, щоб відповісти на @hrobky безпосередньо, але STRING_AGG виглядає чудово, проте вона доступна лише в SQL Server 2016 vNext. Сподіваємось, він незабаром перейде до Azure SQL Datababse ..


2
Я щойно тестував це, і він працює як шарм у базі даних SQL Azure
milanio

4
STRING_AGG()заявляється, що стає доступним у SQL Server 2017, на будь-якому рівні сумісності. docs.microsoft.com/en-us/sql/t-sql/functions/…
CVn

1
Так. STRING_AGG не доступний в SQL Server 2016.
Магне

2

Ви можете використовувати + = для об'єднання рядків, наприклад:

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

якщо ви вибрали @test, він дасть вам усі імена, об'єднані


Вкажіть діалект або версію 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
;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.