我正在寻找一种方法将不同行的字符串聚合成一行。我想在很多不同的地方做这个,所以有一个功能来促进这将是很好的。我尝试过使用COALESCE
和FOR XML
的解决方案,但他们不会为我剪掉它。
字符串聚合会执行以下操作:
id | Name Result: id | Names
-- - ---- -- - -----
1 | Matt 1 | Matt, Rocks
1 | Rocks 2 | Stylus
2 | Stylus
我已将CLR-defined aggregate functions视为COALESCE
和FOR XML
的替代,但显然 SQL Azure 不会支持CLR定义的东西,这对我来说很痛苦,因为我知道能够使用它会为我解决很多问题。
是否有任何可能的解决方法,或类似的最佳方法(可能不如CLR那样最优,但是嘿我将采取我能得到的)我可以用来聚合我的东西?
答案 0 :(得分:59)
<强>解强>
最佳的定义可能有所不同,但这里是如何使用常规的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
答案 1 :(得分:43)
使用FOR XML PATH的方法是否真的那么慢? Itzik Ben-Gan写道,这种方法在他的T-SQL查询书中表现良好(在我看来,Ben-Gan先生是一个值得信赖的来源)。
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
答案 2 :(得分:27)
对于我们这些发现此且未使用Azure SQL数据库的人:
STRING_AGG()
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()
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
答案 3 :(得分:22)
虽然@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 :(得分:5)
更新:Ms SQL Server 2017+,Azure SQL数据库
您可以使用:STRING_AGG
。
对于OP的请求,使用非常简单:
SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id
好吧,我的旧答案被正确地删除了(下面留下了原因),但如果将来有人碰巧登陆这里,那就有好消息了。他们也在Azure SQL数据库中建立了STRING_AGG()。这应该提供本帖子中最初请求的具有本机和内置支持的确切功能。 @hrobky当时曾将此作为SQL Server 2016功能提及。
---老邮报: 这里没有足够的声誉来直接回复@hrobky,但是STRING_AGG看起来很棒,但它目前仅在SQL Server 2016 vNext中可用。希望很快它也将遵循Azure SQL Datababse ..
答案 5 :(得分:1)
您可以使用+ =来连接字符串,例如:
declare @test nvarchar(max)
set @test = ''
select @test += name from names
如果您选择@test,它将为您提供连接的所有名称
答案 6 :(得分:1)
我发现Serge的答案非常有希望,但我也遇到了写作的性能问题。但是,当我重新构建它以使用临时表而不包括双CTE表时,性能从1分40秒变为亚秒,以获得1000个组合记录。这里适用于任何需要在旧版本的SQL Server上没有FOR XML的情况下执行此操作的人:
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
;