连接/聚合字符串的最佳方法


102

我正在寻找一种将不同行中的字符串聚合为一行的方法。我希望在许多不同的地方进行此操作,因此具有促进此操作的功能会很好。我已经尝试过使用COALESCE和解决方案FOR XML,但它们只是不适合我。

字符串聚合将执行以下操作:

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus

我看过CLR定义的聚合函数来代替COALESCEFOR XML,但是显然SQL Azure 支持CLR定义的东西,这让我很痛苦,因为我知道能够使用它可以解决很多问题。我的问题。

有什么可能的解决方法,或者类似的最优方法(可能不如CLR最优,但是我会尽力而为)来聚合我的东西?


哪种方式for xml对您不起作用?
Mikael Eriksson

4
它确实有效,但是我看了执行计划,每个计划for xml显示查询性能(查询的大部分!)的使用率为25%
matt 2012年

2
有多种执行for xml path查询的方法。一些比其他更快。它可能取决于您的数据,但根据distinct我的经验,正在使用的数据要比使用慢group by。而且,如果您.value('.', nvarchar(max))要获取级联值,则应将其更改为.value('./text()[1]', nvarchar(max))
Mikael Eriksson

3
您接受的答案类似于我在stackoverflow.com/questions/11137075/…上的答案,我认为这比XML更快。不要被查询成本所迷惑,您需要足够的数据才能看到哪个更快。XML更快,恰好是@MikaelEriksson 对同一问题回答。选择XML方法
Michael Buen

2

Answers:


67

最佳的定义可能有所不同,但是以下是使用常规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

说明

该方法可归纳为三个步骤:

  1. 使用数字对行进行编号,OVERPARTITION根据需要对它们进行分组和排序。结果是PartitionedCTE。我们保留每个分区中的行数,以便以后过滤结果。

  2. 使用递归CTE(Concatenated)遍历行号(NameNumber列),将Name值添加到FullName列中。

  3. 过滤掉所有结果,但结果最高的那些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

5
我对照xmlpath检查了这种方法的时间消耗,我达到了大约4毫秒,而大约是54毫秒。因此,xmplath方法特别适合在大型情况下使用。我将在单独的答案中编写比较代码。
QMaster 2014年

更好的方法是,这种方法最多只能使用100个值。
RomanoZumbé2014年

@romano-zumbé使用MAXRECURSION将CTE限制设置为您需要的任何值。
Serge Belov 2014年

1
令人惊讶的是,CTE对我而言要慢得多。sqlperformance.com/2014/08/t-sql-queries/…比较了一堆技术,似乎与我的结果一致。
Nickolay

对于记录数超过一百万的表,此解决方案不起作用。此外,我们对递归深度的限制
Ardalan Shahgholi

51

像下面这样使用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

id一旦表的大小成为问题,不要忘记在该列上添加索引。
milivojeviCH 2012年

1
在阅读了填充/ XML路径的工作原理之后(stackoverflow.com/a/31212160/1026),尽管它的名称为XML ,我相信这是一个很好的解决方案:)
Nickolay

1
@slackterman取决于要操作的记录数。与CTE相比,我认为XML在数量较少时是有缺陷的,但在数量较大时,如果正确且简洁地进行,则可以减轻递归部门的限制,并且更易于浏览。
GoldBishop

如果您的数据中包含表情符号或特殊/替代字符,则FOR XML PATH方法会崩溃!!!
devinbost

1
此代码导致xml编码的文本(&切换为&,依此类推)。这里for xml提供更正确的解决方案。
弗雷德里克

33

对于那些发现这个的人 并且没有使用Azure SQL数据库

STRING_AGG()在PostgreSQL,SQL Server 2017和Azure SQL中使用
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.microsoft.com/zh-cn/sql/t-sql/函数/ 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


1
我刚刚对其进行了测试,现在它可以与Azure SQL数据库一起正常工作。
milanio

5
STRING_AGG被推迟到2017年。2016
。– Morgan Thrapp '17

1
谢谢Aamir和Morgan Thrapp的SQL Server版本更改。更新。(在撰写本文时,它声称在2016版中受支持。)
Hrobky

25

尽管@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

2
+1,您就是(黑暗艺术)的QMaster!我得到了更大的差异。(在Intel Xeon E5-2630 v4 @ 2.20 GHZ x2 w /〜1 GB可用空间上,Windows Server 2008 R2上的SQL Server 2008 R2上的〜3000毫秒CTE与XML上的〜70毫秒XML)仅建议:1)在两个版本中都使用OP或(最好是)通用术语,2)由于OP的Q.是如何“连接/聚合字符串 ”,并且仅对于字符串(相对于数字值)才需要术语笼统了。只需使用“ GroupNumber”和“ StringValue”,3)声明并使用“ Delimiter”变量,并使用“ Len(Delimiter)”和“ 2”。
汤姆(Tom)

1
+1表示不将特殊字符扩展为XML编码(例如,在许多其他劣等解决方案中,“&”不会扩展为“&amp;”)
反向工程师,

13

更新: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。


2
我刚刚对其进行了测试,它在Azure SQL数据库中就像一个魅力一样工作
milanio

4
STRING_AGG()声明可以任何兼容级别在SQL Server 2017中可用。docs.microsoft.com/en-us/sql/t-sql/functions/...
一个CVN

1
是。STRING_AGG在SQL Server 2016中不可用。–
Magne

2

您可以使用+ =连接字符串,例如:

declare @test nvarchar(max)
set @test = ''
select @test += name from names

如果选择@test,它将为您提供所有串联的名称


请指定SQL方言或版本,因为该版本受支持。
Hrobky

这在SQL Server 2012中有效。请注意,可以使用select @test += name + ', ' from names
Art Schmidt

4
这会使用未定义的行为,因此不安全。如果ORDER BY查询中包含,这很可能会给出奇怪/错误的结果。您应该使用列出的替代方法之一。
Dannnno

1
此类查询从未定义过行为,并且在SQL Server 2019中我们发现它比以前的版本具有更一致的错误行为。不要使用这种方法。
马修·罗达图斯

2

我发现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
;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.