「EXISTS(…)OR EXISTS(…)」内の句の順序


11

2つの事柄の1つが存在するかどうかをテストするクエリのクラスがあります。それは形です

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

実際のステートメントはCで生成され、ODBC接続を介してアドホッククエリとして実行されます。

ほとんどの場合、2番目のSELECTは最初のSELECTよりも高速であり、2つのEXISTS句の順序を切り替えると、先ほど作成した1つ以上の不正なテストケースで劇的なスピードアップが発生したことが最近判明しました。

明らかなことは、先に進んで2つの句を切り替えることですが、SQL Serverに詳しいユーザーがこれを検討する必要があるかどうかを確認したかったのです。偶然と「実装の詳細」に頼っているような気がします。

(また、SQL Serverの方が賢い場合は、両方のEXISTS句を並行して実行し、どちらかが最初に完了したときに、もう一方を短絡させます。)

SQL Serverがこのようなクエリの実行時間を一貫して改善するためのより良い方法はありますか?

更新

お時間と私の質問に関心をお寄せいただきありがとうございます。実際のクエリプランについての質問は期待していませんでしたが、共有したいと思っています。

これは、SQL Server 2008R2以降をサポートするソフトウェアコンポーネント用です。データの形状は、構成と使用法によってかなり異なる場合があります。(例では)dbf_1162761$z$rv$1257927703テーブルには常にテーブルよりも多くの行数が含まれるため、私の同僚はクエリにこの変更を加えることを考えましたdbf_1162761$z$dd$1257927703

ここに私が言及した虐待事件があります。最初のクエリは低速で、約20秒かかります。2番目のクエリはすぐに完了します。

それが価値があることについては、パラメータのスニッフィングが特定のケースを破壊していたため、「OPTIMIZE FOR UNKNOWN」ビットも最近追加されました。

元のクエリ:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

元の計画:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

修正されたクエリ:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

固定プラン:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)

回答:


11

一般的な経験則として、SQL ServerはCASEステートメントの一部を順番に実行しますが、OR条件を自由に並べ替えることができます。一部のクエリではWHENCASEステートメント内の式の順序を変更することで、一貫してより良いパフォーマンスを得ることができます。ORステートメントの条件の順序を変更すると、パフォーマンスが向上する場合もありますが、動作が保証されているわけではありません。

簡単な例で説明するのがおそらく最善です。私はSQL Server 2016に対してテストを行っているので、マシンでまったく同じ結果が得られない可能性がありますが、私が知る限り、同じ原則が適用されます。まず、2つのテーブルに1から1000000までの100万の整数を配置します。1つはクラスター化インデックスを使用し、もう1つはヒープとして使用します。

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

次のクエリについて考えてみます。

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

特に一致する行がない場合、サブクエリの評価は、サブクエリの評価X_CIよりもはるかに安くなることがわかっていX_HEAPます。一致する行がない場合は、クラスター化インデックスを持つテーブルに対して数回の論理読み取りを実行するだけで済みます。ただし、一致する行がないことを確認するには、ヒープのすべての行をスキャンする必要があります。オプティマイザはこれも知っています。大まかに言えば、クラスター化インデックスを使用して1つの行を検索することは、テーブルをスキャンすることと比較して非常に安価です。

このサンプルデータの場合、クエリを次のように記述します。

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

これにより、SQL Serverは最初にクラスター化インデックスを持つテーブルに対してサブクエリを実行することになります。ここからの結果ですSET STATISTICS IO, TIME ON

テーブル 'X_CI'。スキャンカウント0、論理読み取り3、物理読み取り0

SQL Server実行時間:CPU時間= 0ミリ秒、経過時間= 0ミリ秒。

クエリプランを見て、ラベル1でのシークがデータを返す場合、ラベル2でのスキャンは不要であり、発生しません。

良いクエリ

次のクエリは効率がはるかに低くなります。

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

クエリプランを見ると、ラベル2でのスキャンが常に行われていることがわかります。行が見つかった場合、ラベル1でのシークはスキップされます。それは私たちが望んだ順序ではありません:

不正なクエリプラン

パフォーマンス結果はそれを裏付けています:

テーブル 'X_HEAP'。スキャンカウント1、論理読み取り7247

SQL Server実行時間:CPU時間= 15ミリ秒、経過時間= 22ミリ秒。

元のクエリに戻ると、このクエリでは、シークとスキャンがパフォーマンスに適した順序で評価されています。

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

そしてこのクエリでは、それらは逆の順序で評価されます:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

ただし、前のクエリのペアとは異なり、SQL Serverクエリオプティマイザーが一方を他方よりも先に評価するよう強制するものはありません。重要なことについては、その動作に依存すべきではありません。

結論として、1つのサブクエリを他のサブクエリより先に評価する必要がある場合は、CASEステートメントまたはその他の方法を使用して、順序付けを強制します。それ以外の場合は、任意のOR条件でサブクエリを自由に順序付けできますが、オプティマイザが記述された順序でサブクエリを実行する保証はありません。

補遺:

当然のことですが、SQL Serverでより安価なクエリを決定し、そのクエリを最初に実行したい場合はどうすればよいでしょうか。一部のメソッドの動作が保証されていない場合でも、これまでのすべてのメソッドは、クエリが書き込まれた順序でSQL Serverによって実装されているようです。

簡単なデモテーブルで機能するように見える1つのオプションを次に示します。

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

ここで db fiddleデモを見つけることができます。派生テーブルの順序を変更しても、クエリプランは変更されません。どちらのクエリでも、X_HEAPテーブルは変更されません。言い換えると、クエリオプティマイザーは最初に安価なクエリを実行するように見えます。このようなものをプロダクションで使用することはお勧めできません。そのため、これは主に好奇心の価値のためにここにあります。同じことを行うにはもっと簡単な方法があるかもしれません。


4
またはCASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END、代替手段になる可能性もありますが、それでも、手動でどのクエリが高速であるかを判断し、そのクエリを最初に置くことに依存しています。SQL Serverが自動的に並べ替え、安価なものが最初に自動的に評価されるように表現する方法があるかどうかはわかりません。
マーティン・スミス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.