문자열을 연결 / 집계하는 최적의 방법


102

다른 행의 문자열을 단일 행으로 집계하는 방법을 찾고 있습니다. 여러 곳에서이 작업을 수행하려고하므로이를 용이하게하는 기능이 있으면 좋을 것입니다. 나는 COALESCE및 을 사용하여 솔루션을 시도 FOR XML했지만 그들은 나를 위해 그것을 자르지 않습니다.

문자열 집계는 다음과 같은 작업을 수행합니다.

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

나는 한 번 봐 찍은 CLR 정의 집계 함수 를 대체 COALESCE하고 FOR XML,하지만 분명히 SQL 애저는 하지 않습니다 내가의 전체를 많이 해결할 사용할 수있는 알고 있기 때문에 나를 위해 고통 CLR 정의 물건을 지원 나를위한 문제.

가능한 해결 방법, 또는 (CLR과 같은 최적으로되지 않을 수도 있지만, 마찬가지로 최적의 방법이있다 헤이 나는 내 물건을 집계하는 데 사용할 수있는 내가 무엇을 얻을 수 할게요)는?


어떤 방식으로 for xml당신에게 효과가 없습니까?
Mikael Eriksson

4
작동하지만 실행 계획을 살펴 보았고 각각 for xml쿼리 성능 측면에서 25 %의 사용량을 보여줍니다 (대량 쿼리!)
matt

2
for xml path쿼리 를 수행하는 방법에는 여러 가지가 있습니다. 일부는 다른 것보다 빠릅니다. 그것은 귀하의 데이터에 따라 달라질 수 있지만 사용하는 것은 사용하는 것 distinct보다 느립니다 group by. 당신이 사용하는 경우 그리고 .value('.', nvarchar(max))연결 한 문자열 값을 얻기 위해 당신에게 그 변경해야합니다.value('./text()[1]', nvarchar(max))
미카엘 에릭손

3
귀하의 허용 대답은 내 유사 대답stackoverflow.com/questions/11137075/... 나는 빠른 XML보다 생각했다. 쿼리 비용에 속지 마십시오. 어느 것이 더 빠른지 확인하려면 충분한 데이터가 필요합니다. XML은 더 빠르며 동일한 질문 에 대한 @MikaelEriksson의 대답 입니다 . XML 접근 방식을 선택
마이클 부엔

2
여기에서 네이티브 솔루션에 투표하십시오. connect.microsoft.com/SQLServer/feedback/details/1026336
JohnLBevan 2014

답변:


67

해결책

최적 의 정의 는 다를 수 있지만 다음은 Azure에서 제대로 작동하는 일반 Transact SQL을 사용하여 다른 행의 문자열을 연결하는 방법입니다.

;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. 연결을 위해 필요에 따라 행을 사용 OVER하고 PARTITION그룹화하고 순서를 지정합니다. 결과는 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 개의 값에 대해서만 작동하므로 훨씬 좋습니다.
Romano Zumbé 2014 년

@ romano-zumbé MAXRECURSION을 사용하여 CTE 제한을 필요한 것으로 설정하십시오.
Serge Belov 2014 년

1
놀랍게도 CTE는 저에게 훨씬 느 렸습니다. sqlperformance.com/2014/08/t-sql-queries/… 여러 기술을 비교하고 내 결과에 동의하는 것 같습니다.
Nickolay

1 백만 개 이상의 레코드가있는 테이블에 대한이 솔루션은 작동하지 않습니다. 또한, 우리는 재귀 깊이에 제한이
Ardalan Shahgholi

51

아래와 같이 FOR XML PATH를 사용하는 방법이 정말 느리나요? Itzik Ben-Gan은 T-SQL Querying 책에서이 방법이 좋은 성능을 가지고 있다고 썼습니다 (Mr. 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

1
그리고 물건 / XML 경로 일 (얼마나 읽은 후 stackoverflow.com/a/31212160/1026 ), 나는 그것이 :) 이름에 XML에도 불구하고 좋은 해결책이 있다고 확신
Nickolay

1
@slackterman 작업 할 레코드 수에 따라 다릅니다. 저는 XML이 CTE에 비해 낮은 수에서 부족하다고 생각하지만, 높은 볼륨 수에서는 재귀 부서 제한을 완화하고 정확하고 간결하게 수행하면 탐색하기가 더 쉽습니다.
GoldBishop

데이터에 이모티콘이나 특수 / 대리 문자가 있으면 FOR XML PATH 메서드가 폭발합니다 !!!
devinbost

1
이 코드는 xml로 인코딩 된 텍스트 ( &로 전환 등 &)가됩니다. 여기에 더 정확한 for xml솔루션이 제공 됩니다 .
Frédéric

33

이것을 발견 한 우리를 위해 Azure SQL Database를 사용하지 않습니다.:

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

(Azure 업데이트를 위해 @Brianjorden 및 @milanio에게 감사드립니다)

예제 코드 :

select Id
, STRING_AGG(Name, ', ') Names 
from Demo
group by Id

SQL Fiddle : http://sqlfiddle.com/#!18/89251/1


1
방금 테스트했으며 이제 Azure SQL Database에서 제대로 작동합니다.
milanio

5
STRING_AGG2016 년에는 사용할 수 없습니다.
Morgan Thrapp

1
SQL Server 버전 변경에 대해 Aamir와 Morgan Thrapp에게 감사드립니다. 업데이트되었습니다. (작성 당시 버전 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 (다크 아츠) 당신! 나는 훨씬 더 극적인 차이를 얻었습니다. (~ 3000msec CTE vs. Intel Xeon E5-2630 v4 @ 2.20GHZ x2 (최대 1GB 여유 공간 포함)에서 Windows Server 2008 R2의 SQL Server 2008 R2에서 ~ 70msec XML). 제안 사항은 다음과 같습니다. 1) 두 버전 모두에 대해 OP 또는 (바람직하게는) 일반 용어를 사용합니다. 2) OP의 Q.는 " 문자열을 연결 / 집계"하는 방법이므로 문자열 (vs. 숫자 값) 에만 필요하므로 일반 용어가 너무 일반적입니다. "GroupNumber"및 "StringValue"를 사용하십시오. 3) "Delimiter"변수를 선언하고 사용하고 "Len (Delimiter)"대 "2"를 사용하십시오.
Tom

1
특수 문자를 XML 인코딩으로 확장하지 않는 경우 +1 (예 : 다른 많은 열등한 솔루션에서와 같이 '&'이 '& amp;'로 확장되지 않음)
Reversed Engineer

13

업데이트 : Ms SQL Server 2017+, Azure SQL Database

다음을 사용할 수 있습니다 STRING_AGG..

OP의 요청에 대한 사용법은 매우 간단합니다.

SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id

더 읽어보기

글쎄요, 제 예전 무답은 제대로 삭제되었지만 (아래에 그대로 남아 있음), 만약 누군가가 미래에 여기에 착륙한다면 좋은 소식이 있습니다. Azure SQL Database에서도 STRING_AGG ()를 구현했습니다. 이 게시물에서 원래 요청 된 기능을 기본 및 내장 지원과 함께 제공해야합니다. @hrobky는 이전에이를 당시 SQL Server 2016 기능으로 언급했습니다.

--- 이전 게시물 : @hrobky에 직접 답장하기에는 평판이 충분하지 않지만 STRING_AGG는 훌륭해 보이지만 현재 SQL Server 2016 vNext에서만 사용할 수 있습니다. 조만간 Azure SQL Datababse를 따르기를 바랍니다.


2
난 그냥 그것을 테스트했고 그것은 푸른 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 2019

1
이러한 유형의 쿼리는 정의 된 동작이 아니 었으며 SQL Server 2019에서는 이전 버전보다 일관되게 잘못된 동작이있는 것으로 나타났습니다. 이 접근 방식을 사용하지 마십시오.
Matthew Rodatus

2

나는 Serge의 대답이 매우 유망하다는 것을 알았지 만 작성된대로 성능 문제가 발생했습니다. 그러나 이중 CTE 테이블을 포함하지 않고 임시 테이블을 사용하도록 재구성했을 때 1000 개의 결합 된 레코드에 대해 성능이 1 분 40 초에서 1 초 미만으로 떨어졌습니다. 이전 버전의 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
;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.