为什么 [看似] 合适的索引不用于带有 OR 的 LEFT JOIN

SEa*_*986 4 sql-server optimization execution-plan sql-server-2019 query-performance

我在 StackOverflow 数据库中有以下 [相当无意义,仅用于演示] 查询:

SELECT  *
FROM    Users u
        LEFT JOIN Comments c
            ON u.Id = c.UserId OR
               u.Id = c.PostId
WHERE   u.DisplayName = 'alex'
Run Code Online (Sandbox Code Playgroud)

Users表上唯一的索引是 ID 上的聚集索引。

Comments表具有以下非聚集索引以及 ID 上的聚集索引:

CREATE INDEX IX_UserID ON Comments
(
    UserID,
    PostID
)

CREATE INDEX IX_PostID ON Comments
(
    PostID,
    UserID
)
Run Code Online (Sandbox Code Playgroud)

查询的估计计划在这里

我可以看到优化器将做的第一件事是对用户表执行 CI 扫描以仅过滤那些用户 where DisplayName = Alex,有效地执行此操作:

SELECT  *
FROM    Users u
WHERE   u.DisplayName = 'alex'
ORDER BY Id
Run Code Online (Sandbox Code Playgroud)

并检索结果如下:

在此处输入图片说明

然后它会扫描评论 CI 并针对每一行,查看该行是否满足谓词

u.Id = c.UserId OR u.Id = c.PostId
Run Code Online (Sandbox Code Playgroud)

尽管有两个索引,但仍会执行此 CI 扫描。

如果优化器对上面 Comments 表中的每个索引进行单独的查找并将它们连接在一起,不是更有效吗?

如果我想象一下它会是什么样子,在上面的屏幕截图中,我们可以看到用户 CI 扫描的第一个结果是 ID 420

我可以想象IX_UserID使用索引的样子

SELECT      UserID,
            PostID
FROM        Comments
ORDER BY    UserID,
            PostID
Run Code Online (Sandbox Code Playgroud)

因此,如果我查找用户 ID 420 的行作为索引查找将:

在此处输入图片说明

对于每一行 where UserID = 420,我可以查看 u.Id = c.UserId OR u.Id = c.PostId它们是否都匹配u.Id = c.UserId我们谓词的一部分,

因此,对于索引查找的第二部分,我们可以通过索引进行查找,IX_PostID其可视化如下:

SELECT      PostID,
            UserID
FROM        Comments
ORDER BY    PostID,
            UserID 
Run Code Online (Sandbox Code Playgroud)

如果我寻求发布 ID 420,我什么也看不到:

在此处输入图片说明

所以我们然后回到 CI 扫描的结果,移动到下一行(userId 447)并重复这个过程。

我上面描述的行为可以在WHERE子句中使用:

SELECT      UserID,
            PostID
FROM        Comments
WHERE       UserID = 420 OR PostID = 420
ORDER BY    UserID,
            PostID
Run Code Online (Sandbox Code Playgroud)

在这里计划

因此,我的问题是,为什么子句中的OR条件JOIN不能对适当的索引执行索引查找?

Jos*_*ell 5

而不是专注于如何改进这样的查询,这是其他答案正在做的事情,我将尝试回答被问到的问题:为什么优化器不会产生像你所描述的那样的计划(扫描 Users 表,然后查找 Comments 表上的两个索引)。

这是您的原始查询(请注意,我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);
Run Code Online (Sandbox Code Playgroud)

和计划:

原始左连接计划的屏幕截图

  • dbo.Users使用残差谓词扫描以仅获取“alex”用户
  • 对于这些用户中的每一个,扫描dbo.Comments表并过滤连接运算符中的匹配项
  • 估计成本:293.161 个优化器单元

获得您想要的计划的一种尝试是尝试在桌子上强制搜索dbo.Comments

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

计划是这样的:

带有提示的左连接计划的屏幕截图

  • 扫描dbo.Users表(使用残差谓词只获取名为“alex”的用户),
  • 查找两个索引中的每一个以获取请求的 Id 值(它们联合在一起)
  • 接下来是键查找以获取其余的列(因为我们选择了 *)
  • 估计成本:5.98731 个优化器单元

所以答案是优化器绝对有能力产生这样的计划。而且它似乎不是基于成本的决定(寻找计划看起来便宜得多)。

我最好的猜测是,这只是优化器探索过程中的某种限制——它似乎不赞成将带有 or 子句的左连接转换为应用。在这种特殊情况下,这真的很不幸,因为扫描计划(在我的机器上查询需要 45 秒)与应用计划(不到 1 秒)的性能很差。

旁注:您可以使用未记录的跟踪标志 8726 覆盖不利于索引联合计划的启发式方法。有关该方面的其他详细信息,请参阅https://dba.stackexchange.com/a/23779

正如 Rob Farley 很有帮助地指出的那样,APPLY直接使用(也可能与 a 一起使用UNION)是获得您正在寻找的计划的更好方法 - 这两种方法都会产生该计划(FORCESEEK版本)的“更好”版本。我会说“ ORin a JOIN”是一种已知的反模式,应该避免使用,因为优化器似乎并没有直接支持这种类型的查询。

  • @SEarle1986 谢谢!顺便说一下,根据 Paul 的相关帖子,看起来优化器的搜索过程中确实内置了一种启发式方法,它不喜欢这种索引联合计划。这不是 MS 文档,但它可能与我们将获得的官方文档一样接近 (2认同)