여러 행의 열을 단일 행으로 결합


14

좀 있어요 customer_comments인해 데이터베이스 설계에 여러 행으로 분할을하고, 보고서의 난을 결합해야 comments각각의 고유 한에서 id하나의 행으로. 이전에 SELECT 절과 COALESCE 트릭 에서이 구분 된 목록으로 작업하는 것을 시도했지만 기억할 수 없으므로 저장하지 않아야합니다. 이 경우에도 작동하지 않는 것처럼 보이며 단일 행에서만 작동하는 것 같습니다.

데이터는 다음과 같습니다.

id  row_num  customer_code comments
-----------------------------------
1   1        Dilbert        Hard
1   2        Dilbert        Worker
2   1        Wally          Lazy

내 결과는 다음과 같아야합니다.

id  customer_code comments
------------------------------
1   Dilbert        Hard Worker
2   Wally          Lazy

따라서 각각에 대해 row_num실제로는 한 행의 결과 만 있습니다. 주석은 순서대로 결합해야합니다 row_num. 위의 링크 된 SELECT트릭은 특정 쿼리에 대한 모든 값을 하나의 행으로 가져 오는 데 효과적이지만 SELECT모든 행을 뱉어내는 명령문의 일부로 작동시키는 방법을 알 수는 없습니다 .

내 쿼리는 전체 테이블을 자체적으로 통과 하여이 행을 출력해야합니다. 각 행마다 하나씩 여러 열로 결합 PIVOT하지 않으므로 적용 할 수 없습니다.

답변:


18

이것은 상관 된 하위 쿼리와 관련하여 비교적 사소합니다. 언급 한 블로그 게시물에서 강조 표시된 COALESCE 메소드를 사용자 정의 함수로 추출하지 않으면 (또는 한 번에 한 행만 리턴하지 않는 한) 사용할 수 없습니다. 다음은 일반적으로 수행하는 방법입니다.

DECLARE @x TABLE 
(
  id INT, 
  row_num INT, 
  customer_code VARCHAR(32), 
  comments VARCHAR(32)
);

INSERT @x SELECT 1,1,'Dilbert','Hard'
UNION ALL SELECT 1,2,'Dilbert','Worker'
UNION ALL SELECT 2,1,'Wally','Lazy';

SELECT id, customer_code, comments = STUFF((SELECT ' ' + comments 
    FROM @x AS x2 WHERE id = x.id
     ORDER BY row_num
     FOR XML PATH('')), 1, 1, '')
FROM @x AS x
GROUP BY id, customer_code
ORDER BY id;

의견의 데이터가 안전하지 않은를 위해 XML 문자를 포함 할 수있는 곳이 케이스가있는 경우 ( >, <, &), 당신은이를 변경해야합니다 :

     FOR XML PATH('')), 1, 1, '')

이보다 정교한 접근 방식 :

     FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')

(적절한 대상 데이터 유형 varchar또는 nvarchar올바른 길이를 N사용 하고을 사용하는 경우 모든 문자열 리터럴을 접두어로 사용하십시오 nvarchar.)


3
+1 빠른보기를 위해 바이올린을 읽었습니다 sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
네, 이것은 매력처럼 작동합니다. @MarlonRibunal SQL Fiddle이 정말 형성 중입니다!
벤 Brocka

@NickChammas-목을 내밀어 order by하위 쿼리에서 in을 사용하여 순서가 보장된다고 말합니다 . 이것은 XML을 사용하여 구축 for xml하고 있으며 TSQL을 사용하여 XML을 구축 하는 방법입니다. XML 파일에서 요소의 순서는 중요한 문제이며 신뢰할 수 있습니다. 따라서이 기술이 순서를 보장하지 않으면 TSQL의 XML 지원이 심각하게 손상됩니다.
Mikael Eriksson 2016 년

2
기본 테이블의 클러스터형 인덱스에 관계없이 쿼리가 올바른 순서로 결과를 반환하는지 확인했습니다 (클러스터형 인덱스 도 Mikael이 제안한대로 row_num desc준수해야 함 order by). 쿼리에 권리가 포함되어 있으며 order by@JonSeigel이 동일한 작업을 수행하기를 희망한다는 의견이 있으면 제거 하겠습니다.
Aaron Bertrand

6

사용자 환경에서 CLR을 사용할 수있는 경우 이는 사용자 정의 집계에 대한 맞춤형 사례입니다.

특히 소스 데이터가 사소하지 않고 응용 프로그램에서 이러한 유형의 작업을 많이 수행해야하는 경우이 방법이 필요할 수 있습니다. Aaron의 솔루션에 대한 쿼리 계획 이 입력 크기가 커짐에 따라 확장되지 않을 것이라고 강력하게 생각합니다 . (나는 임시 테이블에 색인을 추가하려고 시도했지만 도움이되지 않았습니다.)

이 솔루션은 다른 많은 것들과 마찬가지로 트레이드 오프입니다.

  • 귀하 또는 귀하의 클라이언트 환경에서 CLR 통합을 사용하기위한 정치 / 정책.
  • CLR 기능은 더 빠를 수 있으며 실제 데이터 세트를 고려할 때 더 잘 확장됩니다.
  • CLR 함수는 다른 쿼리에서 재사용 할 수 있으며 이러한 유형의 작업을 수행해야 할 때마다 복잡한 하위 쿼리를 복제 (및 디버깅) 할 필요가 없습니다.
  • Straight T-SQL은 외부 코드를 작성하고 관리하는 것보다 간단합니다.
  • 아마도 C # 또는 VB로 프로그래밍하는 방법을 모른다.
  • 기타

편집 : 글쎄, 이것이 실제로 더 나은지 보려고 노력했으며 주석이 특정 순서로 있어야한다는 요구 사항이 현재 집계 함수를 사용하여 만족시킬 수 없다는 것이 밝혀졌습니다. :(

SqlUserDefinedAggregateAttribute.IsInvariantToOrder를 참조하십시오 . 기본적으로, 당신이해야 할 것은 OVER(PARTITION BY customer_code ORDER BY row_num)있지만 ORDER BY에서 지원되지 않습니다 OVER집계 때 절. 이 기능을 SQL Server에 추가하면 실행 계획에서 변경해야 할 것이 사소한 일이기 때문에 웜을 열 수 있다고 가정합니다. 위에서 언급 한 링크는 향후 사용을 위해 예약되어 있으므로 향후에 구현 할 수 있다고 말합니다 (2005 년에는 운이 좋지 않을 수도 있음).

이것은 여전히 집계 된 문자열로 값을 패킹하고 파싱 한 다음 CLR 객체 내에서 정렬을 수행하여 달성 할 수 있습니다 row_num...

어쨌든 아래는 제한이 있어도 다른 사람이 유용하다고 생각하는 경우에 사용한 코드입니다. 나는 해킹 부분을 독자를위한 연습으로 남겨 둘 것이다. 테스트 데이터에는 AdventureWorks (2005)를 사용했습니다.

집계 어셈블리 :

using System;
using System.IO;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace MyCompany.SqlServer
{
    [Serializable]
    [SqlUserDefinedAggregate
    (
        Format.UserDefined,
        IsNullIfEmpty = false,
        IsInvariantToDuplicates = false,
        IsInvariantToNulls = true,
        IsInvariantToOrder = false,
        MaxByteSize = -1
    )]
    public class StringConcatAggregate : IBinarySerialize
    {
        private string _accum;
        private bool _isEmpty;

        public void Init()
        {
            _accum = string.Empty;
            _isEmpty = true;
        }

        public void Accumulate(SqlString value)
        {
            if (!value.IsNull)
            {
                if (!_isEmpty)
                    _accum += ' ';
                else
                    _isEmpty = false;

                _accum += value.Value;
            }
        }

        public void Merge(StringConcatAggregate value)
        {
            Accumulate(value.Terminate());
        }

        public SqlString Terminate()
        {
            return new SqlString(_accum);
        }

        public void Read(BinaryReader r)
        {
            this.Init();

            _accum = r.ReadString();
            _isEmpty = _accum.Length == 0;
        }

        public void Write(BinaryWriter w)
        {
            w.Write(_accum);
        }
    }
}

테스트 용 T-SQL ( CREATE ASSEMBLYsp_configureCLR을 사용하지 않도록 설정) :

CREATE TABLE [dbo].[Comments]
(
    CustomerCode int NOT NULL,
    RowNum int NOT NULL,
    Comments nvarchar(25) NOT NULL
)

INSERT INTO [dbo].[Comments](CustomerCode, RowNum, Comments)
    SELECT
        DENSE_RANK() OVER(ORDER BY FirstName),
        ROW_NUMBER() OVER(PARTITION BY FirstName ORDER BY ContactID),
        Phone
        FROM [AdventureWorks].[Person].[Contact]
GO

CREATE AGGREGATE [dbo].[StringConcatAggregate]
(
    @input nvarchar(MAX)
)
RETURNS nvarchar(MAX)
EXTERNAL NAME StringConcatAggregate.[MyCompany.SqlServer.StringConcatAggregate]
GO


SELECT
    CustomerCode,
    [dbo].[StringConcatAggregate](Comments) AS AllComments
    FROM [dbo].[Comments]
    GROUP BY CustomerCode

1

여기에 의해 주석의 순서를 보장하는 커서 기반 솔루션이 row_num있습니다. ( 테이블을 채우는 방법에 대한 다른 답변 을 참조하십시오 [dbo].[Comments].)

SET NOCOUNT ON

DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT
        CustomerCode,
        Comments
        FROM [dbo].[Comments]
        ORDER BY
            CustomerCode,
            RowNum

DECLARE @curCustomerCode int
DECLARE @lastCustomerCode int
DECLARE @curComment nvarchar(25)
DECLARE @comments nvarchar(MAX)

DECLARE @results table
(
    CustomerCode int NOT NULL,
    AllComments nvarchar(MAX) NOT NULL
)


OPEN cur

FETCH NEXT FROM cur INTO
    @curCustomerCode, @curComment

SET @lastCustomerCode = @curCustomerCode


WHILE @@FETCH_STATUS = 0
BEGIN

    IF (@lastCustomerCode != @curCustomerCode)
    BEGIN
        INSERT INTO @results(CustomerCode, AllComments)
            VALUES(@lastCustomerCode, @comments)

        SET @lastCustomerCode = @curCustomerCode
        SET @comments = NULL
    END

    IF (@comments IS NULL)
        SET @comments = @curComment
    ELSE
        SET @comments = @comments + N' ' + @curComment

    FETCH NEXT FROM cur INTO
        @curCustomerCode, @curComment

END

IF (@comments IS NOT NULL)
BEGIN
    INSERT INTO @results(CustomerCode, AllComments)
        VALUES(@curCustomerCode, @comments)
END

CLOSE cur
DEALLOCATE cur


SELECT * FROM @results

0
-- solution avoiding the cursor ...

DECLARE @idMax INT
DECLARE @idCtr INT
DECLARE @comment VARCHAR(150)

SELECT @idMax = MAX(id)
FROM [dbo].[CustomerCodeWithSeparateComments]

IF @idMax = 0
    return
DECLARE @OriginalTable AS Table
(
    [id] [int] NOT NULL,
    [row_num] [int] NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

DECLARE @FinalTable AS Table
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

INSERT INTO @FinalTable 
([customer_code])
SELECT [customer_code]
FROM [dbo].[CustomerCodeWithSeparateComments]
GROUP BY [customer_code]

INSERT INTO @OriginalTable
           ([id]
           ,[row_num]
           ,[customer_code]
           ,[comment])
SELECT [id]
      ,[row_num]
      ,[customer_code]
      ,[comment]
FROM [dbo].[CustomerCodeWithSeparateComments]
ORDER BY id, row_num

SET @idCtr = 1
SET @comment = ''

WHILE @idCtr < @idMax
BEGIN

    SELECT @comment = @comment + ' ' + comment
    FROM @OriginalTable 
    WHERE id = @idCtr
    UPDATE @FinalTable
       SET [comment] = @comment
    WHERE [id] = @idCtr 
    SET @idCtr = @idCtr + 1
    SET @comment = ''

END 

SELECT @comment = @comment + ' ' + comment
        FROM @OriginalTable 
        WHERE id = @idCtr

UPDATE @FinalTable
   SET [comment] = @comment
WHERE [id] = @idCtr

SELECT *
FROM @FinalTable

2
커서를 피하지 않았습니다. 방금 커서를 while 루프라고 불렀습니다.
Aaron Bertrand
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.