web-dev-qa-db-ja.com

OR

StackOverflowデータベースに次の[デモの目的でのみ、かなり意味のない]クエリがあります。

SELECT  *
FROM    Users u
        LEFT JOIN Comments c
            ON u.Id = c.UserId OR
               u.Id = c.PostId
WHERE   u.DisplayName = 'alex'

Usersテーブルの唯一のインデックスは、IDのクラスター化インデックスです。

Commentsテーブルには、次の非クラスター化インデックスとIDのクラスター化インデックスがあります。

CREATE INDEX IX_UserID ON Comments
(
    UserID,
    PostID
)

CREATE INDEX IX_PostID ON Comments
(
    PostID,
    UserID
)

クエリの推定計画は here です。

オプティマイザーが最初に行うことは、usersテーブルでCIスキャンを実行して、DisplayName = Alexであるユーザーのみをフィルターにかけ、これを効果的に行うことです。

SELECT  *
FROM    Users u
WHERE   u.DisplayName = 'alex'
ORDER BY Id

結果をそのように取得する:

enter image description here

次に、コメントCIをスキャンし、すべての行について、行が述部を満たすかどうかを確認します

u.Id = c.UserId OR u.Id = c.PostId

2つのインデックスにもかかわらず、このCIスキャンが実行されます。

オプティマイザが上記のコメントテーブルの各インデックスに対して個別にシークを行い、それらを結合した方が効率的ではないでしょうか。

それがどのように見えるかを視覚化すると、上のスクリーンショットで、ユーザーCIスキャンの最初の結果がID 420であることがわかります。

IX_UserIDインデックスがどのように見えるかを視覚化できます

SELECT      UserID,
            PostID
FROM        Comments
ORDER BY    UserID,
            PostID

したがって、インデックスシークとしてユーザーID 420の行をシークすると、次のようになります。

enter image description here

UserID = 420のすべての行について、u.Id = c.UserId OR u.Id = c.PostIdが存在するかどうかを確認できます。もちろん、これらすべてが述語のu.Id = c.UserId部分に一致します。

したがって、インデックスシークの2番目の部分では、次のように視覚化できるインデックスIX_PostIDをシークできます。

SELECT      PostID,
            UserID
FROM        Comments
ORDER BY    PostID,
            UserID 

ID 420を投稿しようとすると、何も表示されません。

enter image description here

したがって、CIスキャンの結果に戻り、次の行(userId 447)に移動して、プロセスを繰り返します。

上記で説明した動作は、WHERE句で使用できます。

SELECT      UserID,
            PostID
FROM        Comments
WHERE       UserID = 420 OR PostID = 420
ORDER BY    UserID,
            PostID

ここで計画

したがって、私の質問は、OR句のJOIN条件が適切なインデックスでインデックスシークを実行できない理由です。

4
SEarle1986

このようなクエリを改善する方法に焦点を当てるのではなく、他の回答が行っていることを行うのではなく、尋ねられている質問に答えようとします:whyは、オプティマイザーは、先ほど説明したような計画を作成します(ユーザーテーブルをスキャンし、コメントテーブルの2つのインデックスを検索します)。

これが元のクエリです(実行プランで見たものをシミュレートするためだけにMAXDOP 2を使用していることに注意してください)。

SELECT  *
FROM    Users u
        LEFT JOIN Comments c
            ON u.Id = c.UserId OR
               u.Id = c.PostId
WHERE   u.DisplayName = 'alex'
OPTION (MAXDOP 2);

そして計画:

Screenshot of original left join plan

  • 「alex」ユーザーのみを取得するための、残余述語を含むdbo.Usersのスキャン
  • これらの各ユーザーについて、dbo.Commentsテーブルをスキャンし、結合演算子で一致をフィルタリングします
  • 推定コスト:293.161オプティマイザユニット

必要な計画を取得する1つの試みは、dbo.Commentsテーブルでシークしてforceシークすることです。

SELECT  *
FROM    Users u
        LEFT JOIN Comments c WITH (FORCESEEK)
            ON u.Id = c.UserId OR
               u.Id = c.PostId
WHERE   u.DisplayName = 'alex'
OPTION (MAXDOP 2);

計画は次のようになります。

Screenshot of left join plan with hints

  • dbo.Usersテーブルのスキャン( "alex"という名前のユーザーのみを取得するための残余述語を含む)、
  • 2つのインデックスのそれぞれをシークして、要求されたId値(結合されたもの)を取得します。
  • キーの検索が続き、残りの列を取得します(*を選択したため)。
  • 推定コスト:5.98731オプティマイザユニット

したがって、答えは、オプティマイザがそのような計画を確実に作成できることです。そして、それはコストベースの決定ではないようです(シーク計画ははるかに安く見えます)。

私の推測では、これはオプティマイザの探索プロセスにおける一種の制限であると思われます。or句を使用した左結合を適用に変換するのは好都合ではないようです。これは本当にスキャンプラン(クエリは私のマシンで45秒かかります)と適用プラン(1秒未満)ではパフォーマンスが悪いため、この特定のケースでは残念です。

サイドノート:文書化されていないトレースフラグ8726を使用して、インデックスユニオンプランを不快にするヒューリスティックを上書きできます。詳細については、 https://dba.stackexchange.com/a/23779 を参照してください。その前に!

Rob Farleyが有益に指摘したように、APPLYを直接(場合によってはUNIONも使用して)使用することは、あなたが探している計画を取得するためのより良いアプローチです-どちらも「より良い」を生み出しますこのプランのバージョン(FORCESEEKバージョン)。 「OR in a JOIN」は既知のアンチパターンの一種であり、オプティマイザがそのタイプのクエリを適切にサポートしていないように思われるため、避ける必要があります直接。

5
Josh Darnell

結合がある場合、クエリオプティマイザーは、さまざまな結合手法に関連する述語を満たす方法を検討します。これは、APPLYで記述された場合と同じようにクエリを再評価しようとするものではありません。これは、結合の右側がサブクエリのように見えるため、ここで必要なものです。

あなたは自分でこれを試すことができます:

SELECT  *
FROM    Users u
    OUTER APPLY (
        SELECT *
        FROM Comments c
        WHERE u.Id = c.UserId 
        OR u.Id = c.PostId
    ) c
WHERE   u.DisplayName = 'alex'

... ORよりも与えられているため、迷惑なことにUNIONに変換されないことがよくあります。

SELECT  *
FROM    Users u
    OUTER APPLY (
        SELECT *
        FROM Comments c
        WHERE u.Id = c.UserId 
        UNION
        SELECT *
        FROM Comments c
        WHERE u.Id = c.PostId
    ) c
WHERE   u.DisplayName = 'alex'
4
Rob Farley
  1. TSQLオプティマイザは、結合ごとに1つのインデックスのみを使用できます。どのインデックスが絶対的に最高であるかが確実であれば、ヒントによってオプティマイザにそれを伝えることができます
SELECT  *
FROM    Users u
        LEFT JOIN Comments c with (index ([IX_UserID] )) 
            ON u.Id = c.UserId OR
               u.Id = c.PostId
WHERE   u.DisplayName = 'alex'
  1. 結合全体でOR句を使用すると、インデックスの有用性がなくなります。これは、インデックス全体をスキャンしてから、見つかった行のすべてのフィールドを検索することしかできないためです。テーブルスキャンだけと比較して、その有効性を予測することは困難です。
  2. クエリを2つのクエリに分割し、それぞれをORとは異なる条件を使用してから、UNIONを使用してそれらを結合します。そこでは両方のインデックスを使用する必要があります。
SELECT  *
FROM    Users u  LEFT JOIN Comments c  ON u.Id = c.UserId 
WHERE   u.DisplayName = 'alex'

union
SELECT  *
FROM    Users u  LEFT JOIN Comments c ON  u.Id = c.PostId
WHERE   u.DisplayName = 'alex'

0
finesoul