データベースの設計により、いくつかのcustomer_comments
が複数の行に分割されています。レポートの場合、各固有のcomments
からのid
を1つの行に結合する必要があります。私は以前にこれを使って何かを試しました 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
は該当しないようです。
これは、相関サブクエリを使用する場合と比べて簡単です。ユーザー定義関数に抽出しない限り(または一度に1行だけを返す必要がない限り)、ブログの投稿で強調表示されている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
を付けてください。 。)
ご使用の環境でCLRの使用が許可されている場合、これはユーザー定義集計のテーラーメイドのケースです。
特に、ソースデータが自明でないほど大きい場合や、アプリケーションでこの種のことをたくさん行う必要がある場合は、おそらくこれが適切な方法です。 Aaronのソリューション のクエリプランは、入力サイズが大きくなるとうまくスケーリングしないと強く思います。 (一時テーブルにインデックスを追加しようとしましたが、役に立ちませんでした。)
このソリューションは、他の多くのことと同様に、トレードオフです。
編集:ええと、私はこれが実際に優れているかどうかを確認しようとしましたが、コメントが特定の順序になっている必要があることがわかりました集計関数を使用して満足することはできません。 :(
SqlUserDefinedAggregateAttribute.IsInvariantToOrder を参照してください。基本的に、行う必要があるのはOVER(PARTITION BY customer_code ORDER BY row_num)
ですが、OVER
句では集計時にORDER BY
がサポートされていません。この機能をSQL Serverに追加すると、ワームの缶が開くと想定しています。実行計画で変更する必要があるのは簡単なことだからです。前述のリンクでは、これは将来の使用のために予約されているので、将来実装される可能性があります(ただし、2005年には、おそらく運が悪いでしょう)。
このcouldは、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(CLRを有効にするためのCREATE Assembly
、およびsp_configure
省略):
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
以下は、row_num
によるコメントの順序を保証するカーソルベースのソリューションです。 ([dbo].[Comments]
テーブルがどのように入力されたかについては、my other answer を参照してください。)
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
-- 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