连接/聚合string的最佳方式
我正在find一种方法来将不同行中的string聚合成一行。 我正在寻找在许多不同的地方这样做,所以有一个function,以促进这将是很好的。 我已经尝试了使用COALESCE
和FOR XML
解决scheme,但是他们并没有为我裁剪。
string聚合会做这样的事情:
id | Name Result: id | Names -- - ---- -- - ----- 1 | Matt 1 | Matt, Rocks 1 | Rocks 2 | Stylus 2 | Stylus
我已经看了CLR定义的聚合函数来代替COALESCE
和FOR XML
,但显然SQL Azure 不支持CLR定义的东西,这对我来说是一个痛苦,因为我知道能够使用它会解决很多问题给我。
是否有任何可能的解决方法,或类似的最佳方法(可能不如CLR最佳,但嘿,我会拿我能得到的),我可以用来聚合我的东西?
解
最优化的定义可能会有所不同,但以下是如何使用常规Transact SQL连接来自不同行的string,这应该在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
分组对行进行编号,并根据需要对它们进行sorting以进行串联。 结果是Partitioned
CTE。 我们保留每个分区中的行数,以便稍后过滤结果。 -
使用recursionCTE(
Concatenated
)遍历行号(NameNumber
列)将Name
值添加到FullName
列。 -
筛选出所有结果,但是具有最高
NameNumber
。
请记住,为了使这个查询是可预测的,你必须定义两个分组(例如,在你的场景中,具有相同ID
行被连接)和sorting(我假设你只是在串联之前按字母顺序sortingstring)。
我已经使用以下数据快速testing了SQL Server 2012上的解决scheme:
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
使用下面的FOR XML PATH的方法真的很慢吗? Itzik Ben-Gan写道,这种方法在他的T-SQL查询书(在我看来,Ben-Gan先生是一个值得信赖的来源)有很好的performance。
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 #tt group by id
尽pipe@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
对于我们这些谁发现这一点 并没有使用Azure SQL数据库 :
PostgreSQL中的STRING_AGG()
,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
MySQL中的GROUP_CONCAT()
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat
(感谢@Brianjorden和@milanio的Azure更新)
那么我的旧的不答复已经被正确地删除了(下面留下的),但是如果有将来在这里降临的话,这是一个好消息。 它们也在Azure SQL数据库中实现了STRING_AGG()。 这应该提供本文所要求的确切function,并具有本地和内置的支持。 @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
—旧post:这里没有足够的信誉直接回复@hrobky,但是STRING_AGG看起来不错,但是它目前仅在SQL Server 2016 vNext中可用。 希望它能尽快跟上Azure SQL Datababse。