web-dev-qa-db-ja.com

複数列の連結

複数の列を単一の行に連結する方法は?例えば:

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

これを防ぐ方法はありますか?

6
Biju jose

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分強で実行されました。

5
Kenneth Fisher

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を構築してから再解析するよりも大幅に安価です。

6
Martin Smith

マーティン・スミスがすでに指摘したように、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;

ケネスのソリューションとほぼ同じ時間で実行されます。

1
spaghettidba

これを試して

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 bydistinctで置き換えることもできます。これは、aggregate関数を使用していないためです。

0