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

Lui*_*rao 43 performance sql-server

TL; 博士

由于这个问题一直有意见,我在这里总结一下,这样新人就不必忍受历史:

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

我意识到这可能不是每个人的问题,但通过突出 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
Run Code Online (Sandbox Code Playgroud)

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

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

实际执行计划

如您所见,几乎 90% 的时间都花在了嵌套循环(内连接)上。以下是有关这些嵌套循环的额外信息:

嵌套循环(内连接)

请注意,表名并不完全匹配,因为我编辑了查询表名以提高可读性,但很容易匹配 (ads_alt_category = category)。有没有办法优化这个查询?另请注意,在生产中,临时表 #Ids 不存在,它是传递给存储过程的相同 10'000 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)
Run Code Online (Sandbox Code Playgroud)

这个微小的调整是最快的解决方案,针对已接受答案的双重连接进行了测试,并根据valverij 的建议针对 CROSS APPLY 进行了测试。

Gor*_*off 19

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

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

orin 加入条件总是可疑的。一个建议是将其拆分为两个连接:

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

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

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

val*_*rij 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
)
Run Code Online (Sandbox Code Playgroud)

除了将这些拆分为多个连接之外,您还可以尝试 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
Run Code Online (Sandbox Code Playgroud)

从上面的 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但是,使用,它将在找到匹配项的瞬间停止子查询。