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

mat*_*att 91 sql sql-server aggregate-functions azure azure-sql-database

我正在寻找一种方法将不同行的字符串聚合成一行.我想在很多不同的地方做这个,所以有一个功能来促进这将是很好的.我尝试使用COALESCE和的解决方案FOR XML,但他们只是不为我削减它.

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

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus
Run Code Online (Sandbox Code Playgroud)

我已经看过CLR定义的聚合函数作为替代COALESCEFOR XML,但显然SQL Azure 支持CLR定义的东西,这对我来说很痛苦因为我知道能够使用它会解决很多问题.我的问题.

是否有任何可能的解决方法,或类似的最佳方法(可能不如CLR那样最优,但嘿,我会采取我能得到的)我可以用来聚合我的东西?

Ser*_*lov 62

最优的定义可以有所不同,但是这里是如何使用常规的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
Run Code Online (Sandbox Code Playgroud)

说明

该方法归结为三个步骤:

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

  2. 使用递归CTE(Concatenated)遍历行号(NameNumber列)向列添加NameFullName.

  3. 筛选出所有结果,但最高的结果NameNumber.

请记住,为了使此查询可预测,必须定义两个分组(例如,在您的方案行中具有相同ID的连接)和排序(我假设您只是在连接之前按字母顺序对字符串进行排序).

我已经使用以下数据快速测试了SQL Server 2012上的解决方案:

INSERT dbo.SourceTable (ID, Name)
VALUES 
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')
Run Code Online (Sandbox Code Playgroud)

查询结果:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
Run Code Online (Sandbox Code Playgroud)

  • 我检查了这种方式对xmlpath的时间消耗,我达到了大约4毫秒,大约54毫秒.所以xmplath方式在大型情况下特别好.我将在单独的答案中写出比较代码. (5认同)

sla*_*man 46

使用类似下面的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
Run Code Online (Sandbox Code Playgroud)

  • 在阅读了 stuff/for xml 路径是如何工作的(http://stackoverflow.com/a/31212160/1026)之后,我相信它是一个很好的解决方案,尽管它的名字是 XML :) (3认同)

Hro*_*bky 29

对于我们这些发现这一点的人 并且没有使用Azure SQL数据库:

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/功能/串-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
Run Code Online (Sandbox Code Playgroud)

SQL小提琴:http://sqlfiddle.com/#!18/89251/1

  • `STRING_AGG`被推迟到2017年.它在2016年不可用. (4认同)

QMa*_*ter 24

虽然@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
Run Code Online (Sandbox Code Playgroud)

这是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
Run Code Online (Sandbox Code Playgroud)

  • +1,你是QMaster(黑魔法)你!我有一个更戏剧性的差异.(在Intel Xeon E5-2630 v4 @ 2.20 GHZ x2 w/~1 GB免费的Windows Server 2008 R2上,约为3000毫秒CTE与SQL Server 2008 R2上的~70毫秒XML).只有以下建议:1)对两个版本使用OP或(最好)通用术语,2)因为OP的Q.是如何"连接/聚合*字符串*"而这仅仅是*字符串*(对比a*)数字*值),通用术语*太*通用.只需使用"GroupNumber"和"StringValue",3)声明并使用"Delimiter"变量并使用"Len(Delimiter)"与"2". (2认同)

Bri*_*den 7

更新:SQL Server 2017+女士,Azure SQL数据库

您可以使用:STRING_AGG

对于OP的请求,用法非常简单:

SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id
Run Code Online (Sandbox Code Playgroud)

阅读更多

好吧,我原来的非回答已被正确删除(下面保留完整无缺),但是如果将来有人碰巧落在这里,那就是个好消息。他们还在Azure SQL数据库中隐含了STRING_AGG()。那应该提供本帖最初要求的确切功能,以及本机和内置的支持。@hrobky当时曾将此作为SQL Server 2016功能提到。

---旧帖子:这里的信誉不足,无法直接回复@hrobky,但STRING_AGG看起来不错,但是目前仅在SQL Server 2016 vNext中可用。希望它也将很快应用于Azure SQL Datababse。

  • 声明“ STRING_AGG()”以任何兼容性级别在SQL Server 2017中可用。https://docs.microsoft.com/zh-cn/sql/t-sql/functions/string-agg-transact-sql (3认同)
  • 我刚刚测试了它,它在Azure SQL数据库中就像一个魅力 (2认同)
  • 是的。STRING_AGG 在 SQL Server 2016 中不可用。 (2认同)

jvc*_*jvc 6

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

declare @test nvarchar(max)
set @test = ''
select @test += name from names
Run Code Online (Sandbox Code Playgroud)

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

  • 这使用了未定义的行为,并且不安全。如果查询中有“ORDER BY”,则特别有可能给出奇怪/不正确的结果。您应该使用列出的替代方案之一。 (4认同)