web-dev-qa-db-ja.com

SELECTで複数のCASEステートメントを処理する効率的な方法

基本的にキーまたはIDである列値に基づいて、マッピングIDテーブルから対応する値をフェッチする必要がある状況でレポートを実行します。以下のようなもの:

    SELECT
     (case when [column1='A'] then (select value from Table1) 
           when [column1='B'] then (select value from Table2) 
           when [column1='C'] then (select value from Table3)

           and so on uptil 35 more 'when' conditions ...

  ELSE column1 end) Value 
from Table1

より正確に:

SELECT 
(case when [A.column1='1'] 
then (select value from B where B.clientId=100 and A.column1=B.Id) 
when [A.column1='2'] 
then (select value from C where C.clientId=100 and A.column1=C.Id) 
when [A.column1='3'] 
then (select value from D where D.clientId=100 and A.column1=D.Id) 
...
and so on uptil 30 more 'when' conditions 
...
ELSE column1 end) 
FROM A

テーブルB、C、D ..などでは、すべてのクライアントのデータを保持しています。各クライアントには特定のClientIdがあり、これらのテーブルB、C、DなどにはId列とClientId列にインデックスが設定されています。

SQL Serverでこれを処理する効率的な方法はありますか?

3
Abhi

私は、サブクエリのテーブルに適切なインデックスがあると想定しています。いくつかのクイックテストデータをモックアップして、テーブルAに1000万行を入れました。私は30個のテーブルを作成するゲームではなかったので、CASE式用に3個を作成しました。一般的な原則を示すには3で十分だと思います。

_DROP TABLE IF EXISTS dbo.B;
CREATE TABLE dbo.B (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO B VALUES (100, '1', 'TABLE B'); 

DROP TABLE IF EXISTS dbo.C;
CREATE TABLE dbo.C (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO C VALUES (100, '2', 'TABLE C'); 

DROP TABLE IF EXISTS dbo.D;
CREATE TABLE dbo.D (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO D VALUES (100, '3', 'TABLE D'); 


DROP TABLE IF EXISTS dbo.A;
CREATE TABLE dbo.A (
    column1 VARCHAR(20) NOT NULL
);

INSERT INTO dbo.A WITH (TABLOCK)
SELECT CAST(1 + t.RN % 3 AS VARCHAR(20))
FROM
(
    SELECT TOP (5000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;
_

結果セットを無効にして、SSMSで次のクエリを実行しました。

_SELECT A.column1
FROM A;
_

約0.723秒かかりました。私はあなたのデータを何も知らないので、かなり非科学的なテストをしています。いずれにせよ、シリアルクエリでは0.7秒以上の結果は期待できません。それが私たちのベースラインです。

このクエリを作成する最も効率的な方法は、結合をまったく使用しない方法です。重要なのは、CASE式は、一致が見つかった場合にのみ3(または30)の一意の値を返すことです。結果をローカル変数に保存して、クエリで使用することができます。以下のクエリは約1.044秒で終了します。

_DECLARE @B_VALUE VARCHAR(100) = (select value from B where B.clientId=100 and B.Id = '1');
DECLARE @C_VALUE VARCHAR(100) = (select value from C where C.clientId=100 and C.Id = '2');
DECLARE @D_VALUE VARCHAR(100) = (select value from D where D.clientId=100 and D.Id = '3');

SELECT 
(case when A.column1='1' then @B_VALUE
      when A.column1='2' then @C_VALUE
      when A.column1='3' then @D_VALUE
      -- omitted other columns
else column1 end) 
FROM A;
_

計画は非常に簡単です:

enter image description here

別のオプションとして、結合を使用してクエリを記述できます(CASE式をCOALESCE()を使用してよりコンパクトな形式に書き換えることができます。これは約2.314秒で終了しました:

_SELECT 
    COALESCE(B.column1, C.column1, D.column1, -- omitted other columns  
             A.column1)
    -- (case A.column1
    --    when '1' then B.value
    --    when '2' then C.value
    --    when '3' then D.value
    --    -- omitted other columns
    -- else A.column1 end) 
FROM A
LEFT JOIN B ON B.clientId=100 and B.Id = '1'
LEFT JOIN C ON C.clientId=100 and C.Id = '2'
LEFT JOIN D ON D.clientId=100 and D.Id = '3';
_

ここに計画があります:

enter image description here

次のようなクエリを記述することで、ほぼ同じランタイムとクエリプランを取得できます。

_SELECT 
(case A.column1
   when '1' then (select value from B where B.clientId=100 and '1'=B.Id) 
   when '2' then (select value from C where C.clientId=100 and '2'=C.Id) 
   when '3' then (select value from D where D.clientId=100 and '3'=D.Id) 
-- omitted other columns
else column1 end) 
FROM A;
_

問題の元のクエリには問題があります。SQLServerはネストされたループ結合の前に無用なソートを行っています。このクエリは、私のマシンでは約5.838秒で終了します。

enter image description here

トレースフラグ8690は、並べ替えとテーブルスプールを削除します。クエリはトレースフラグ8690で約7.479秒で実行されるため、スプールはこのクエリに役立つと思います。

6
Joe Obbish

それらがすべて異なるテーブルである場合、これが最良のケースのシナリオである可能性があります。 UNLESS Table1.column1の値は繰り返すことができます(複数行= '1'など)。その場合、サブクエリから結合に移動することができます。

SELECT CASE 
    WHEN A.column1='1' THEN B.value
    WHEN A.column1='2' THEN C.value
    WHEN A.column1='3' THEN D.value
    and so on uptil 30 more 'when' conditions 
    ELSE A.column1
    END 
FROM A
    LEFT JOIN B ON A.column1='1' AND A.column1=B.Id AND B.clientId=100
    LEFT JOIN C ON A.column1='2' AND A.column1=C.Id AND C.clientId=100
    LEFT JOIN D ON A.column1='2' AND A.column1=D.Id AND D.clientId=100
4
indiri