在 JOIN 子句中使用 OR 时出现奇怪的查询计划 - 对表中的每一行进行持续扫描

And*_*rew 11 sql-server execution-plan sql-server-2017

我正在尝试生成一个示例查询计划来说明为什么联合两个结果集比在 JOIN 子句中使用 OR 更好。我写的一个查询计划让我很难过。我正在使用 StackOverflow 数据库和 Users.Reputation 上的非聚集索引。

查询计划图片 查询是

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
Run Code Online (Sandbox Code Playgroud)

查询计划位于https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE,我的查询持续时间为 4:37 分钟,返回 26612 行。

我以前从未见过从现有表创建这种类型的常量扫描 - 我不熟悉为什么对每一行都运行常量扫描,而常量扫描通常用于用户输入的单行例如 SELECT GETDATE()。为什么用在这里?我非常感谢阅读此查询计划的一些指导。

如果我将 OR 拆分为 UNION,它会生成一个标准计划,运行 12 秒,返回相同的 26612 行。

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
Run Code Online (Sandbox Code Playgroud)

我将这个计划解释为这样做:

  • 从 Posts 中获取所有 41782500 行(实际行数与 Posts 上的 CI 扫描匹配)
  • 对于帖子中的每 41782500 行:
    • 产生标量:
    • Expr1005:所有者用户 ID
    • Expr1006:所有者用户 ID
    • Expr1004:静态值 62
    • Expr1008:LastEditorUserId
    • Expr1009:LastEditorUserId
    • Expr1007:静态值 62
  • 在串联中:
    • Exp1010:如果 Expr1005 (OwnerUserId) 不为空,则使用 E​​xpr1008 (LastEditorUserID)
    • Expr1011:如果 Expr1006 (OwnerUserId) 不为 null,则使用它,否则使用 E​​xpr1009 (LastEditorUserId)
    • Expr1012:如果 Expr1004 (62) 为空,则使用它,否则使用 E​​xpr1007 (62)
  • 在计算标量中:我不知道 & 符号的作用。
    • Expr1013: 4 [and?] 62 (Expr1012) = 4 and OwnerUserId IS NULL (NULL = Expr1010)
    • Expr1014: 4 [and?] 62 (Expr1012)
    • Expr1015:16 和 62 (Expr1012)
  • 在 Order By 排序方式中:
    • Expr1013 描述
    • Expr1014 升序
    • Expr1010 升序
    • Expr1015 描述
  • 在合并间隔中,它删除了 Expr1013 和 Expr1015(这些是输入但不是输出)
  • 在嵌套循环连接下方的索引查找中,它使用 Expr1010 和 Expr1011 作为查找谓词,但我不明白当它没有完成从 IX_NC_REPUTATION 到包含 Expr1010 和 Expr1011 的子树的嵌套循环连接时,它如何访问这些.
  • 嵌套循环联接仅返回在较早的子树中具有匹配项的 Users.ID。由于谓词下推,返回从 IX_NC_REPUTATION 上的索引查找返回的所有行。
  • 最后一个嵌套循环连接:对于每个 Posts 记录,输出 Users.Id,其中在下面的数据集中找到匹配项。

Mar*_*ith 10

该计划类似于我在此处更详细介绍的计划

Posts表被扫描。

对于每一行,它提取OwnerUserIdLastEditorUserId。这与工作方式类似UNPIVOT。您会在下面的计划中看到一个常量扫描运算符,为每个输入行创建两个输出行。

SELECT *
FROM dbo.Posts
UNPIVOT (X FOR U IN (OwnerUserId,LastEditorUserId)) Unpvt
Run Code Online (Sandbox Code Playgroud)

在这种情况下,计划有点复杂,因为它的语义or是如果两个列值相同,则连接中只应发出一行Users(而不是两行)

这些然后通过合并间隔,以便在值相同的情况下,范围被折叠起来,并且只对它执行一次搜索 Users- 否则对其执行两次搜索。

价值 62是一个标志,意味着该搜索应该是一个平等搜索。

关于

当它没有完成从 IX_NC_REPUTATION 到包含 Expr1010 和 Expr1011 的子树的嵌套循环连接时,我不明白它是如何访问这些的

这些是在黄色突出显示的连接运算符中定义的。这是在黄色突出显示的嵌套循环的外侧。所以这在嵌套循环内部的黄色突出显示之前运行。

在此处输入图片说明

提供类似计划的重写(尽管合并间隔被合并联合替换)如下以防万一。

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) 
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

根据Posts表上可用的索引,此查询的变体可能比您提出的UNION ALL解决方案更有效。(我拥有的数据库副本对此没有有用的索引,建议的解决方案对 进行了两次完整扫描Posts。以下是一次扫描)

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
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明