如何优化在嵌套循环(内部联接)上运行缓慢的查询


39

TL; DR

由于这个问题一直在引起人们的关注,因此在这里我将对其进行总结,这样新来的人就不必经历历史了:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

我意识到这可能不是每个人的问题,但是通过强调ON子句的敏感性,它可以帮助您朝正确的方向看。无论如何,原始文本在这里供将来的人类学家使用:

原文

考虑以下简单查询(仅涉及3个表)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

这是一个非常简单的查询,唯一令人困惑的部分是最后一个类别联接,之所以这样,是因为类别级别5可能存在或可能不存在。在查询结束时,我正在寻找每个产品ID(SKU ID)的类别信息,这就是超大表category_link出现的地方。最后,表#Ids只是一个包含10'000 ID的临时表。

执行后,我得到以下实际执行计划:

实际执行计划

如您所见,几乎90%的时间都用在嵌套循环(内部联接)中。以下是有关这些嵌套循环的更多信息:

嵌套循环(内部联接)

请注意,表名并不完全匹配,因为我编辑了查询表名以提高可读性,但是非常容易匹配(ads_alt_category = category)。有什么方法可以优化此查询?还要注意,在生产中,临时表#Ids不存在,它是传递给存储过程的相同10000个ID的表值参数。

附加信息:

  • category_id和parent_category_id上的类别索引
  • category_id,language_code上的category_voc索引
  • sku_id,category_id上的category_link索引

编辑(已解决)

正如公认的答案所指出的那样,问题出在category_link JOIN中的OR子句。但是,接受的答案中建议的代码非常慢,甚至比原始代码还慢。更快,更清洁的解决方案是简单地用以下命令替换当前的JOIN条件:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

这一分钟的调整是最快的解决方案,针对公认答案中的两次联接进行了测试,也针对Valverij建议的“交叉应用”进行了测试。


我们将需要查看其余的查询计划。
RBarryYoung

只是一句话:由于有许多依赖连接,基数估计错误很有可能出现。最常见的是,基数低估了查询性能。
usr

执行计划是否对索引提出建议?另外,不要忘记您可以在临时表上设置主键和索引(更多信息在此处

@rbarry如果尝试了当前解决方案后我什么都没得到,我会改善这个问题

1
如何用UNION复制查询并摆脱OR

Answers:


17

问题似乎出在代码的这一部分:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

or在加入条件中总是可疑的。一种建议是将其分为两个联接:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

然后,您必须修改查询的其余部分以处理此问题。。。coalesce(l1.sku_id, l2.sku_id)例如在select条款中。


通过对特定联接进行大量过滤,我还将测试在的子句中切换JOINCROSS APPLY,然后IN将更改为。EXISTSAPPLYWHERE

谢谢戈登,我会在早上测试这第一件事。@Valverij,我对交叉申请并不熟悉,您能否在适当的答案中更详细地描述您的解决方案,所以如果事实证明是最快的方案,我可以投票吗?

3
我接受这个答案,因为这是第一个向我指出问题的答案。但是,建议的解决方案非常慢,甚至比原始代码还慢。但是,知道OR子句是问题所在,只需将其替换为即可ON l.category_id = ISNULL(c5.category_id, c4.category_id
Luis Ferrao

1
@LuisFerrao。。。感谢您提供其他信息。知道coalesce()将优化器推向正确的方向很有用。
Gordon Linoff

9

正如另一位用户提到的,此连接可能是原因:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

除了将它们拆分为多个联接外,您还可以尝试 CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

从上面的MSDN链接:

表值函数充当右输入,而外部表表达式充当左输入。从左输入的每一行评估右输入,并将产生的行合并为最终输出

基本上,APPLY它就像一个子查询,它首先过滤掉右边的记录,然后将它们应用于查询的其余部分。

本文在解释它是什么以及何时使用它方面做得非常好:http : //explainextended.com/2009/07/16/inner-join-vs-cross-apply/

不过,请务必注意,CROSS APPLY并不一定总是比快INNER JOIN。在许多情况下,它可能大致相同。不过,在极少数情况下,我实际上看到它的速度较慢(再次,这完全取决于您的表结构和查询本身)。

作为一般经验法则,如果我发现自己加入了太多条件语句的表,那么我倾向于 APPLY

还有一个有趣的提示:OUTER APPLY将像一个LEFT JOIN

另外,请注意我选择使用EXISTS而不是IN。在执行IN子查询时,请记住,即使找到了值,它也会返回整个结果集。EXISTS但是,使用时,它将在找到匹配项时立即停止子查询。


我彻底测试了此解决方案。在编写过程中,它的运行速度非常慢,但是却忘记了应用开始发送消息时所使用的建议。更换AND x.cat = c4.cat OR x.cat = c5.catx.cat = ISNULL(c5.cat, c4.cat)并且摆脱IN子句使这个第二个最快的解决方案,值得一给予好评,因为它是相当翔实。
Luis Ferrao

谢谢。实际上不应该在IN行(无法决定使用IN还是坚持使用OR),我将其删除。
valverij
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.