115

我正在寻找一种将不同行中的字符串聚合成一行的方法。我希望在许多不同的地方做到这一点,所以有一个功能来促进这一点会很好。我已经尝试过使用COALESCEand的解决方案FOR XML,但他们只是不适合我。

字符串聚合会做这样的事情:

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

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

是否有任何可能的解决方法或类似的最佳方法(可能不如 CLR 最佳,但,我会尽我所能)可以用来聚合我的东西?

4

7 回答 7

75

解决方案

最佳的定义可能会有所不同,但这里是如何使用常规 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
于 2012-12-03T10:50:07.460 回答
59

像下面这样使用 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
于 2012-12-08T22:59:32.140 回答
50

STRING_AGG()在 SQL Server 2017、Azure SQL 和 PostgreSQL 中: 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()在 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

于 2016-12-13T15:19:50.123 回答
26

虽然@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
于 2014-02-19T09:14:48.240 回答
19

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

于 2016-12-19T14:10:14.767 回答
4

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

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

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

于 2018-03-08T15:20:22.670 回答
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
;
于 2018-05-10T05:44:59.943 回答