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
結果をそのように取得する:
次に、コメント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の行をシークすると、次のようになります。
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を投稿しようとすると、何も表示されません。
したがって、CIスキャンの結果に戻り、次の行(userId 447)に移動して、プロセスを繰り返します。
上記で説明した動作は、WHERE
句で使用できます。
SELECT UserID,
PostID
FROM Comments
WHERE UserID = 420 OR PostID = 420
ORDER BY UserID,
PostID
したがって、私の質問は、OR
句のJOIN
条件が適切なインデックスでインデックスシークを実行できない理由です。
このようなクエリを改善する方法に焦点を当てるのではなく、他の回答が行っていることを行うのではなく、尋ねられている質問に答えようとします: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);
そして計画:
dbo.Users
のスキャンdbo.Comments
テーブルをスキャンし、結合演算子で一致をフィルタリングします必要な計画を取得する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);
計画は次のようになります。
dbo.Users
テーブルのスキャン( "alex"という名前のユーザーのみを取得するための残余述語を含む)、したがって、答えは、オプティマイザがそのような計画を確実に作成できることです。そして、それはコストベースの決定ではないようです(シーク計画ははるかに安く見えます)。
私の推測では、これはオプティマイザの探索プロセスにおける一種の制限であると思われます。or句を使用した左結合を適用に変換するのは好都合ではないようです。これは本当にスキャンプラン(クエリは私のマシンで45秒かかります)と適用プラン(1秒未満)ではパフォーマンスが悪いため、この特定のケースでは残念です。
サイドノート:文書化されていないトレースフラグ8726を使用して、インデックスユニオンプランを不快にするヒューリスティックを上書きできます。詳細については、 https://dba.stackexchange.com/a/23779 を参照してください。その前に!
Rob Farleyが有益に指摘したように、APPLY
を直接(場合によってはUNION
も使用して)使用することは、あなたが探している計画を取得するためのより良いアプローチです-どちらも「より良い」を生み出しますこのプランのバージョン(FORCESEEK
バージョン)。 「OR
in a JOIN
」は既知のアンチパターンの一種であり、オプティマイザがそのタイプのクエリを適切にサポートしていないように思われるため、避ける必要があります直接。
結合がある場合、クエリオプティマイザーは、さまざまな結合手法に関連する述語を満たす方法を検討します。これは、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'
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'
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'