複数の行の列を単一の行に結合する


14

私はいくつか持っているcustomer_commentsため、データベースの設計に複数の行に分割して、レポートのために私は結合する必要がありcomments、それぞれ独自のからをid1行に。以前、SELECT句とCOALESCEトリックからのこの区切りリストで動作するものを試しましたが、思い出せず、保存してはいけません。この場合も動作するようには思えませんが、1行でしか動作しないようです。

データは次のようになります。

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実際にはそれぞれ1行の結果しかありません。コメントはの順序で結合されるべきですrow_num。上記のリンクされたSELECTトリックは、特定のクエリのすべての値を1つの行として取得するために機能しますが、SELECTこれらすべての行を吐き出すステートメントの一部として機能する方法を理解することはできません。

私のクエリは、テーブル全体を単独で調べて、これらの行を出力する必要があります。私はそれらを複数の列(各行に1つ)に結合してPIVOTいませんので、適用できないようです。

回答:


18

これは、相関サブクエリで行うのは比較的簡単です。言及したブログ投稿で強調表示されているCOALESCEメソッドは、ユーザー定義関数に抽出しない限り(または一度に1行のみを返したい場合を除き)使用できません。これが私が通常行う方法です:

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は本当に形を整えています!
ベンブロッカ

@NickChammas-私は首を突き出し、サブクエリでを使用して順序が保証されると言いorder byます。これはを使用for xmlしてXMLを構築することでありそれがTSQLを使用してXMLを構築する方法です。XMLファイル内の要素の順序は重要な問題であり、信頼できます。そのため、この手法で順序が保証されない場合、TSQLでのXMLサポートは著しく破損します。
ミカエルエリクソン

2
基になるテーブルのクラスター化インデックスに関係なく、クエリが正しい順序で結果を返すことを検証しました(クラスター化インデックスでさえ、Mikaelが示唆したとおりrow_num descに従う必要がありますorder by)。クエリに権利が含まれているため、そうでないことを示唆するコメントを削除しorder by、@ JonSeigelが同じことを検討することを期待します。
アーロンバートランド

6

環境でCLRを使用することが許可されている場合、これはユーザー定義集計のテーラーメイドのケースです。

特に、ソースデータが非常に大きくない場合や、アプリケーションでこの種の処理を頻繁に行う必要がある場合に、おそらくこれが適切な方法です。Aaronのソリューションのクエリプランは、入力サイズが大きくなるとうまくスケーリングしないと強く思います。(インデックスを一時テーブルに追加しようとしましたが、助けにはなりませんでした。)

このソリューションは、他の多くのものと同様に、トレードオフです。

  • お客様またはお客様の環境でCLR統合を使用するための政治/ポリシー。
  • CLR関数は高速である可能性が高く、実際のデータセットがあれば、より適切にスケーリングされます。
  • CLR関数は他のクエリで再利用可能であり、この種のことを行う必要があるたびに複雑なサブクエリを複製(およびデバッグ)する必要はありません。
  • ストレートT-SQLは、外部コードを記述して管理するよりも簡単です。
  • おそらく、C#またはVBでプログラムする方法がわからないでしょう。

編集:まあ、私はこれが実際に優れているかどうかを確認しようとしましたが、コメントが特定の順序であるという要件は、集約関数を使用して現在満たすことができないことが判明しました。:(

SqlUserDefinedAggregateAttribute.IsInvariantToOrderを参照してください。基本的に、行う必要OVER(PARTITION BY customer_code ORDER BY row_num)があるのORDER BYOVER、集計時の句でサポートされていません。この機能をSQL Serverに追加すると、実行計画で変更が必要になるのは簡単なため、ワームの缶が開かれると思います。前述のリンクでは、これは将来の使用のために予約されているため、将来実装される可能性があります(ただし、2005年にはおそらく運が悪いでしょう)。

これrow_num値を集計文字列にパックして解析し、CLRオブジェクト内で並べ替えを行うことで実現できます。

いずれにせよ、制限がある場合でも他の誰かがこれを便利だと思う場合に使用するコードを以下に示します。ハッキングの部分は読者の演習として残しておきます。テストデータには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 ASSEMBLY、およびsp_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ループと呼びました。
アーロンバートランド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.