Оптимальный способ конкатенации/объединения строк
Я нахожу способ объединить строки из разных строк в одну строку. Я ищу сделать это во многих разных местах, поэтому иметь функцию, облегчающую это, было бы неплохо. Я пробовал решения, используя 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, но я беру то, что я могу получить), который я могу использовать для объединения моих вещей?
Ответы
Ответ 1
Решение
Определение оптимального может меняться, но здесь как конкатенировать строки из разных строк, используя обычный 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
) повторяется через номера строк (столбец NameNumber
), добавляя значения Name
в столбец FullName
.
-
Отфильтруйте все результаты, кроме тех, которые имеют самый высокий 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
Ответ 2
Являются ли методы, использующие FOR XML PATH, как показано ниже, действительно медленными? Итзик Бен-Ган пишет, что этот метод имеет хорошую производительность в его книге запросов Querying 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
Ответ 3
Хотя ответ @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
Ответ 4
Для тех из нас, кто нашел этот и не использует Azure SQL Database:
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()
в MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat
(Благодаря @Brianjorden и @milanio для обновления Azure)
Ответ 5
Хорошо, мой старый не-ответ был по праву удален (слева внизу), но если кто-то случайно приземлится здесь в будущем, есть хорошие новости. Они также повлияли на STRING_AGG() в базе данных Azure SQL. Это должно обеспечить точную функциональность, первоначально запрошенную в этом сообщении, с помощью встроенной и встроенной поддержки. @hrobky упомянул об этом ранее как функцию SQL Server 2016 в то время.
Использование OP довольно просто:
select id, STRING_AGG(name, ', ') as names
from some_table
group by id
https://msdn.microsoft.com/en-us/library/mt790580.aspx
--- Старый пост:
Недостаточно репутации здесь, чтобы отвечать на @hrobky напрямую, но STRING_AGG выглядит отлично, однако он доступен только в SQL Server 2016 vNext. Надеюсь, что он также скоро последует за Azure SQL Datababse.