Оптимальный способ объединения / агрегирования строк
Я нахожу способ агрегировать строки из разных строк в одну строку. Я хочу сделать это во многих разных местах, поэтому иметь функцию для облегчения этого было бы неплохо. Я пробовал решения с помощью 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 ответов:
решение
определение оптимальный может отличаться, но вот как объединить строки из разных строк с помощью обычного 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
объяснение
подход сводится к трем шагам:
число строк с помощью
OVER
иPARTITION
группировка и упорядочение их по мере необходимости для конкатенации. Этот результатPartitioned
CTE. Мы сохраняем количество строк в каждой секции, чтобы фильтровать результаты позже.использование рекурсивного CTE (
Concatenated
) перебираем номера строк () добавлятьName
значения .отфильтровать все результаты, но те, с самым высоким
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 ;