複数の列を単一の行に連結する方法は?例えば:
id name car
1 sam dodge
1 ram maserati
1 john benz
1 NULL mazda
2 kirk lexus
2 Jim rolls
1 GMC
予想される結果セットは次のとおりです。
ID name car
1 sam,ram,john dodge,maserati,benz,mazda,GMC
2 kirk,jim lexus,rolls
solution を使用すると、スタックオーバーフローで見つかりました:
SELECT * FROM (
SELECT t.id,stuff([m].query('/name').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined1],
stuff([m].query('/car').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined2]
FROM dbo.test t
OUTER apply(SELECT (
SELECT id, ','+name AS name
,','+car AS car
FROM test WHERE test.id=t.id
FOR XML PATH('') ,type)
AS M) A)S
GROUP BY id,somefield_combined1,somefield_combined2
より良い解決策はありますか?内部選択は、高価なマルチテーブル結合(上記の単一のテーブル「テスト」ではない)から行われます。クエリはインラインTVFにあるため、一時テーブルを使用できません。
また、空白の列がある場合、結果は次のような余分なコンマを生成します
ID name car
1 sam,ram,john,, dodge,maserati,benz,mazda,GMC
2 kirk,jim lexus,rolls
これを防ぐ方法はありますか?
600万行強の行を使用して、いくつかのテストを実行しました。 ID列にインデックスがあります。
これが私が思いついたものです。
最初のクエリ:
SELECT * FROM (
SELECT t.id,
stuff([M].query('/name').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined1],
stuff([M].query('/car').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined2]
FROM dbo.test t
OUTER APPLY(SELECT (
SELECT id, ','+name AS name
,','+car AS car
FROM test WHERE test.id=t.id
FOR XML PATH('') ,type)
AS M)
M ) S
GROUP BY id, SomeField_Combined1, SomeField_Combined2
これは約23分間実行されました。
私が最初に学んだバージョンであるこのバージョンを実行しました。いくつかの点でそれはより長くかかるように思えますが、そうではありません。
SELECT test.id,
STUFF((SELECT ', ' + ThisTable.name
FROM test ThisTable
WHERE test.id = ThisTable.id
AND ThisTable.name <> ''
FOR XML PATH ('')),1,2,'') AS ConcatenatedSomeField,
STUFF((SELECT ', ' + car
FROM test ThisTable
WHERE test.id = ThisTable.id
AND ThisTable.car <> ''
FOR XML PATH ('')),1,2,'') AS ConcatenatedSomeField2
FROM test
GROUP BY id
このバージョンは2分強で実行されました。
CLR集約は、ほぼ確実にこれを実行する最速の方法です。しかし、おそらくあなたは何らかの理由でこれを使いたくない...
あなたはこれのソースが高価なクエリであると言います。
最初にこれを#temp
テーブルに具体化して、1回だけ評価されるようにします。
CREATE TABLE #test
(
ID INT,
name NVARCHAR(128),
car NVARCHAR(128)
);
CREATE CLUSTERED INDEX ix ON #test(ID);
質問のクエリで取得した実行プランは、最初に外部クエリのすべての行を連結してから、id, SomeField_Combined1, SomeField_Combined2
によって重複を削除します。
これは非常に無駄です。次のリライトはこれを回避します。
SELECT t.id,
stuff([M].query('/name').value('/', 'varchar(max)'), 1, 1, '') AS [SomeField_Combined1],
stuff([M].query('/car').value('/', 'varchar(max)'), 1, 1, '') AS [SomeField_Combined2]
FROM (SELECT DISTINCT id
FROM #test) t
OUTER APPLY(SELECT (SELECT id,
',' + name AS name,
',' + car AS car
FROM #test
WHERE #test.id = t.id
FOR XML PATH(''), type) AS M) M
ただし、次のテストデータの場合(1000個のID、IDごとに2156行)
INSERT INTO #test
SELECT v.number, o.name, o.type_desc
FROM sys.all_objects o
INNER JOIN master..spt_values v
ON v.type = 'P' AND v.number BETWEEN 1 AND 1000
2つのXML PATH
呼び出しを使用したKennethのソリューションは、はるかに高速でリソースをあまり必要としません。
+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+
| | CPU Time (Seconds) | Elapsed Time (Seconds) | #test Scan Count | #test Logical Reads | Worktable logical reads | Worktable lob logical reads |
+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+
| Single XML PATH | 51.077 | 15.521 | 1,005 | 60,165 | 51,161 | 1,329,207 |
| Double XML PATH | 3.1720 | 3.010 | 2,005 | 92,088 | 14,951 | 233,681 |
+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+
#test
の異なるid
ごとに、1つではなく2つの操作を実行しますが、この操作はXMLを構築してから再解析するよりも大幅に安価です。
マーティン・スミスがすでに指摘したように、CLR集約がおそらく最善の策です。この場合も、結果を一時テーブルに保存することをお勧めします。
UNPIVOT/PIVOTを使用する別のT-SQL実装を次に示します。
IF OBJECT_ID('tempdb..#test') IS NOT NULL
DROP TABLE #test;
CREATE TABLE #test (
id int,
name varchar(128),
car varchar(128)
)
CREATE CLUSTERED INDEX ix ON #test(ID);
INSERT INTO #test
SELECT v.number, o.name, o.type_desc
FROM sys.all_objects o
INNER JOIN master..spt_values v
ON v.type = 'P' AND v.number BETWEEN 1 AND 1000
;WITH info(col) AS (
SELECT 'car'
UNION ALL
SELECT 'name'
)
SELECT *
FROM info
CROSS JOIN (
SELECT DISTINCT id
FROM #test
) AS ids
CROSS APPLY (
SELECT v = STUFF((
SELECT ',' + value AS [text()]
FROM #test
UNPIVOT (value FOR col IN (name,car)) AS u
WHERE col = info.col
AND id = ids.id
AND value <> ''
FOR XML PATH(''), type
).value('.','varchar(max)'),1,1,SPACE(0))
) AS ca(val)
PIVOT (MIN(val) FOR col IN (car,name)) AS p;
ケネスのソリューションとほぼ同じ時間で実行されます。
これを試して
Right
関数の代わりにxml
関数を使用して、先頭のコンマを削除します
case
ステートメントを使用して、空白スペースのカンマを避けます
SELECT t.id,
RIGHT(A.NAME, Len(A.NAME) - 1) AS NAME,
RIGHT(A.car, Len(A.car) - 1) AS car
FROM dbo.test t
OUTER apply(SELECT (SELECT id,
CASE WHEN NAME<>'' THEN ',' ELSE '' END + NAME AS NAME,
CASE WHEN car<>'' THEN ',' ELSE '' END + car AS car
FROM test
WHERE test.id = t.id
FOR xml path(''), type) AS M) A
GROUP BY id,
RIGHT(A.NAME, Len(A.NAME) - 1),
RIGHT(A.car, Len(A.car) - 1)
注:ここにGroup by
はdistinct
で置き換えることもできます。これは、aggregate
関数を使用していないためです。