连接/聚合字符串的最佳方式
Posted
技术标签:
【中文标题】连接/聚合字符串的最佳方式【英文标题】:Optimal way to concatenate/aggregate strings 【发布时间】:2012-11-18 07:44:04 【问题描述】:我正在寻找一种将不同行中的字符串聚合成一行的方法。我希望在许多不同的地方做到这一点,所以有一个功能来促进这一点会很好。我尝试过使用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 最佳,但嘿我会尽我所能)来汇总我的东西?
【问题讨论】:
for xml
在哪些方面不适合您?
它确实有效,但我查看了执行计划,每个for xml
在查询性能方面显示了 25% 的使用率(大部分查询!)
for xml path
查询有不同的方法。有些比其他更快。这可能取决于您的数据,但根据我的经验,使用distinct
的数据比使用group by
慢。如果您使用.value('.', nvarchar(max))
来获取连接值,则应将其更改为.value('./text()[1]', nvarchar(max))
您接受的答案类似于我在***.com/questions/11137075/… 上的answer,我认为它比XML 更快。不要被查询成本所迷惑,您需要充足的数据来查看哪个更快。 XML 更快,恰好是@MikaelEriksson 的answer 在同一个question 上。选择 XML 方法
请在此处投票支持本机解决方案:connect.microsoft.com/SQLServer/feedback/details/1026336
【参考方案1】:
解决方案
optimal 的定义可能会有所不同,但这里介绍了如何使用常规 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
【讨论】:
我检查了这种方式对 xmlpath 的时间消耗,我达到了大约 4 毫秒与大约 54 毫秒。所以 xmpath 方式特别是在大型情况下更好。我将在单独的答案中编写比较代码。 这要好得多,因为这种方法最多只适用于 100 个值。 @romano-zumbé 使用 MAXRECURSION 将 CTE 限制设置为您需要的任何值。 令人惊讶的是,CTE 对我来说要慢得多。 sqlperformance.com/2014/08/t-sql-queries/…比较了一堆技术,似乎和我的结果一致。 对于超过 100 万条记录的表,此解决方案不起作用。此外,我们对递归深度有限制【参考方案2】:像下面这样使用 FOR XML PATH 的方法真的那么慢吗? Itzik Ben-Gan 在他的 T-SQL Querying 一书中写道,这种方法具有良好的性能(在我看来,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
【讨论】:
一旦表的大小成为问题,不要忘记在id
列上放置索引。
在阅读了 stuff/for xml 路径的工作原理 (***.com/a/31212160/1026) 之后,我相信这是一个很好的解决方案,尽管名称中有 XML :)
@slackterman 取决于要操作的记录数。我认为与 CTE 相比,XML 在低计数方面存在缺陷,但在高容量计数方面,缓解了递归部门的限制,并且如果操作正确且简洁,则更易于导航。
如果您的数据中有表情符号或特殊/代理字符,FOR XML PATH 方法就会崩溃!!!
此代码生成 xml 编码的文本(&
切换到 &
,等等)。提供了更正确的for xml
解决方案here。【参考方案3】:
STRING_AGG()
在 SQL Server 2017、Azure SQL 和 PostgreSQL 中:
https://www.postgresql.org/docs/current/static/functions-aggregate.htmlhttps://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 更新)
示例代码:
select Id
, STRING_AGG(Name, ', ') Names
from Demo
group by Id
SQL 小提琴:http://sqlfiddle.com/#!18/89251/1
【讨论】:
我刚刚对其进行了测试,现在它可以在 Azure SQL 数据库中正常运行。STRING_AGG
被推迟到 2017 年。2016 年不可用。
感谢 Aamir 和 Morgan Thrapp 更改 SQL Server 版本。更新。 (在撰写本文时,它声称在 2016 版中受支持。)【参考方案4】:
虽然@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
【讨论】:
+1,你(黑魔法的)QMaster!我得到了一个更加戏剧性的差异。 (约 3000 毫秒 CTE 与约 70 毫秒 XML 上的 SQL Server 2008 R2 上的 Windows Server 2008 R2 上 Intel Xeon E5-2630 v4 @2.20 GHZ x2 w/ ~1 GB 免费)。唯一的建议是:1)两个版本都使用 OP 或(最好)通用术语,2)由于 OP 的 Q. 是如何“连接/聚合 字符串”,而这仅适用于 字符串(相对于数字值),通用术语太通用。只需使用“GroupNumber”和“StringValue”,3)声明并使用“Delimiter”变量并使用“Len(Delimiter)”与“2”。 +1 表示不将特殊字符扩展为 XML 编码(例如,'&' 不会像许多其他劣质解决方案一样扩展为 '&')【参考方案5】:更新:MS SQL Server 2017+,Azure SQL 数据库
您可以使用:STRING_AGG
。
对于 OP 的要求来说,用法很简单:
SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id
Read More
好吧,我的旧答案已被正确删除(保留在下方),但如果将来有人碰巧登陆这里,那就有好消息了。他们还在 Azure SQL 数据库中实现了 STRING_AGG()。这应该提供本帖子中最初要求的确切功能以及本机和内置支持。 @hrobky 之前曾将其作为 SQL Server 2016 的一项功能提到过。
--- 旧帖: 这里没有足够的声誉直接回复@hrobky,但 STRING_AGG 看起来不错,但目前仅在 SQL Server 2016 vNext 中可用。希望它也能尽快跟进 Azure SQL Datababse。
【讨论】:
我刚刚对其进行了测试,它在 Azure SQL 数据库中的作用就像一个魅力STRING_AGG()
被声明在任何兼容级别的 SQL Server 2017 中可用。 docs.microsoft.com/en-us/sql/t-sql/functions/…
是的。 STRING_AGG 在 SQL Server 2016 中不可用。【参考方案6】:
您可以使用 += 来连接字符串,例如:
declare @test nvarchar(max)
set @test = ''
select @test += name from names
如果你选择@test,它会给你所有的名字连接
【讨论】:
请指定SQL方言或版本,从什么时候开始支持。 这适用于 SQL Server 2012。请注意,可以使用select @test += name + ', ' from names
创建逗号分隔列表
这使用了未定义的行为,并且不安全。如果您的查询中有ORDER BY
,这尤其可能会给出奇怪/不正确的结果。您应该使用列出的替代方案之一。
这种类型的查询从未定义过行为,在 SQL Server 2019 中,我们发现它比以前的版本更一致地具有不正确的行为。不要使用这种方法。【参考方案7】:
我发现 Serge 的答案很有希望,但我也遇到了编写时的性能问题。但是,当我对其进行重组以使用临时表且不包含双 CTE 表时,1000 条组合记录的性能从 1 分 40 秒变为亚秒。这里适用于需要在旧版本 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
;
【讨论】:
以上是关于连接/聚合字符串的最佳方式的主要内容,如果未能解决你的问题,请参考以下文章