簡體   English   中英

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

[英]Optimal way to concatenate/aggregate strings

我正在尋找一種方法將來自不同行的字符串聚合到一行中。 我希望在許多不同的地方做到這一點,所以有一個功能來促進這一點會很好。 我嘗試過使用COALESCEFOR XML的解決方案,但它們就是不適合我。

字符串聚合會做這樣的事情:

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

我看過CLR 定義的聚合函數作為COALESCEFOR XML的替代品,但顯然SQL Azure支持 CLR 定義的東西,這對我來說很痛苦,因為我知道能夠使用它可以解決對我來說有很多問題。

是否有任何可能的解決方法或類似的最佳方法(可能不如 CLR 最佳,但,我會盡我所能)來聚合我的東西?

解決方案

最佳的定義可能有所不同,但這里介紹了如何使用常規 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分組對行進行編號,並根據連接的需要對它們進行排序。 結果是Partitioned CTE。 我們保留每個分區中的行數以便稍后過濾結果。

  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

像下面這樣使用 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

SQL Server 2017、Azure SQL 和 PostgreSQL 中的STRING_AGG()https ://www.postgresql.org/docs/current/static/functions-aggregate.html
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql

MySQL 中的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

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

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

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

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

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

如果您選擇@test,它將為您提供所有串聯的名稱

我發現 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
;

試試這個,我在我的項目中使用它

DECLARE @MetricsList NVARCHAR(MAX);

SELECT @MetricsList = COALESCE(@MetricsList + '|', '') + QMetricName
FROM #Questions;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM