web-dev-qa-db-ja.com

JOIN句でORを使用する場合の奇妙なクエリプラン-テーブルのすべての行の定数スキャン

2つの結果セットをUNIONすることが、JOIN句でORを使用するよりも優れている理由である理由を示すために、サンプルのクエリプランを作成しようとしています。作成したクエリプランでは、困惑しています。 Users.Reputationの非クラスター化インデックスを含むStackOverflowデータベースを使用しています。

Query plan image クエリは

CREATE NONCLUSTERED INDEX IX_NC_REPUTATION ON dbo.USERS(Reputation)
SELECT DISTINCT Users.Id
FROM dbo.Users
INNER JOIN dbo.Posts  
    ON Users.Id = Posts.OwnerUserId
    OR Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5

クエリプランは https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE にあり、クエリの実行時間は4:37分、26612行が返されます。

このスタイルの定数スキャンが既存のテーブルから作成されるのを見たことはありません。ユーザーが入力した単一の行に定数スキャンが通常使用されるのに、すべての行に対して一定のスキャンが実行される理由はよくわかりません。たとえば、SELECT GETDATE()です。なぜここで使用されるのですか?このクエリプランを読む際のガイダンスを本当に感謝します。

そのOR=をUNIONに分割すると、同じ26612行が返される12秒で実行される標準計画が作成されます。

SELECT Users.Id
FROM dbo.Users
    INNER JOIN dbo.Posts
       ON Users.Id = Posts.OwnerUserId
WHERE Users.Reputation = 5
UNION 
SELECT Users.Id
FROM dbo.Users
    INNER JOIN dbo.Posts
       ON  Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5

私はこの計画をこれを行うと解釈します:

  • 投稿から41782500行をすべて取得します(実際の行数は投稿のCIスキャンと一致します)
  • 投稿の各41782500行について:
    • スカラーを生成します。
    • Expr1005:OwnerUserId
    • Expr1006:OwnerUserId
    • Expr1004:静的な値62
    • Expr1008:LastEditorUserId
    • Expr1009:LastEditorUserId
    • Expr1007:静的な値62
  • 連結:
    • Exp1010:Expr1005(OwnerUserId)がnullでない場合はそれを使用、それ以外の場合はExpr1008(LastEditorUserID)を使用
    • Expr1011:Expr1006(OwnerUserId)がnullでない場合はそれを使用し、それ以外の場合はExpr1009(LastEditorUserId)を使用します
    • Expr1012:Expr1004(62)がnullの場合はそれを使用し、それ以外の場合はExpr1007(62)を使用します
  • Computeスカラー:アンパサンドの機能がわかりません。
    • Expr1013:4 [および?] 62(Expr1012)= 4およびOwnerUserId IS NULL(NULL = Expr1010)
    • Expr1014:4 [および?] 62(Expr1012)
    • Expr1015:16および62(Expr1012)
  • 並べ替え:
    • Expr1013 Desc
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015 Desc
  • マージ間隔で、Expr1013とExpr1015を削除しました(これらは入力ですが出力ではありません)。
  • ネストされたループ結合の下のインデックスシークでは、Expr1010とExpr1011をシーク述語として使用していますが、IX_NC_REPUTATIONからExpr1010とExpr1011を含むサブツリーへのネストされたループ結合を実行していない場合、これらにアクセスする方法がわかりません。
  • Nested Loops結合は、以前のサブツリーで一致するUsers.IDのみを返します。述語のプッシュダウンのため、IX_NC_REPUTATIONのインデックスシークから返されるすべての行が返されます。
  • 最後のネストされたループ結合:各Postsレコードについて、以下のデータセットで一致が見つかったUsers.Idを出力します。
10
Andrew

計画は、私が行ったものと似ています 詳細はこちら

Postsテーブルがスキャンされます。

各行について、OwnerUserIdおよびLastEditorUserIdを抽出します。これは、UNPIVOTの動作と同様です。以下のプランでは、単一の定数スキャン演算子が表示され、各入力行に2つの出力行が作成されます。

SELECT *
FROM dbo.Posts
UNPIVOT (X FOR U IN (OwnerUserId,LastEditorUserId)) Unpvt

この場合、orのセマンティクスが2つの列の値が同じである場合、Usersの結合から1行のみが出力される必要があるため(2つではなく)、計画は少し複雑になります。

次に、これらはマージ間隔を通過するため、値が同じである場合、範囲は縮小され、Usersに対して1つのシークのみが実行されます。それ以外の場合は、2つのシークが実行されます。

62は、シークが等価シークであることを意味するフラグです。

について

IX_NC_REPUTATIONからExpr1010およびExpr1011を含むサブツリーへのネストされたループ結合を実行していない場合、これらにアクセスする方法がわかりません

これらは、黄色で強調表示された連結演算子で定義されています。これは、黄色で強調表示されたネストされたループの外側にあります。したがって、これは、ネストされたループの内側で黄色で強調表示されたシークの前に実行されます。

enter image description here

これが役立つ場合に備えて、同様の計画を与える書き換え(マージ間隔はマージユニオンに置き換えられています)を以下に示します。

SELECT DISTINCT D2.UserId
FROM   dbo.Posts p
       CROSS APPLY (SELECT Users.Id AS UserId
                    FROM   (SELECT p.OwnerUserId
                            UNION /*collapse duplicate to single row*/
                            SELECT p.LastEditorUserId) D1(UserId)
                           JOIN Users
                             ON Users.Id = D1.UserId) D2
OPTION (FORCE ORDER) 

enter image description here

Postsテーブルで使用できるインデックスに応じて、このクエリのバリアントは、提案されたUNION ALLソリューションよりも効率的です。 (私が持っているデータベースのコピーにはこれに役立つインデックスがなく、提案されたソリューションはPostsの2つのフルスキャンを実行します。以下は1つのスキャンで実行します)

WITH Unpivoted AS
(
SELECT UserId
FROM dbo.Posts
UNPIVOT (UserId FOR U IN (OwnerUserId,LastEditorUserId)) Unpivoted
)
SELECT DISTINCT Users.Id
FROM dbo.Users INNER HASH JOIN Unpivoted
       ON  Users.Id = Unpivoted.UserId
WHERE Users.Reputation = 5

enter image description here

10
Martin Smith