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


10

我正在尝试生成一个示例查询计划,以说明为什么对两个结果集进行UNIONing可能比在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

查询计划位于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

我将此计划解释为:

  • 从帖子中获取所有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)为空,则使用它,否则使用Expr1007(62)
  • 在计算标量中:我不知道“&”号的作用。
    • Expr1013:4 [and?] 62(Expr1012)= 4,并且OwnerUserId为NULL(NULL = Expr1010)
    • Expr1014:4 [和?] 62(Expr1012)
    • Expr1015:16和62(Expr1012)
  • 在排序依据中:
    • Expr1013描述
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015描述
  • 在合并间隔中,它删除了Expr1013和Expr1015(这些是输入但不是输出)
  • 在嵌套循环连接下面的索引查找中,它使用Expr1010和Expr1011作为查找谓词,但是当它没有完成从IX_NC_REPUTATION到包含Expr1010和Expr1011的子树的嵌套循环连接时,我不明白它如何访问这些索引。 。
  • 嵌套循环联接仅返回在较早的子树中具有匹配项的Users.ID。由于谓词下推,将返回从IX_NC_REPUTATION上的索引搜索返回的所有行。
  • 最后一个嵌套循环连接:对于每个Posts记录,输出在以下数据集中找到匹配项的Users.Id。

您尝试使用EXISTS子查询或子查询吗?SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND ( EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.OwnerUserId) OR EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.LastEditorUserId) ) ;
ypercubeᵀᴹ

一个子查询:SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id IN (Posts.OwnerUserId, Posts.LastEditorUserId) ) ;
ypercubeᵀᴹ

Answers:


10

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

Posts表扫描。

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

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

在这种情况下,该计划要复杂一些,因为其语义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) 

在此处输入图片说明

根据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

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.