如何删除大型表的嵌套循环连接

Posted

技术标签:

【中文标题】如何删除大型表的嵌套循环连接【英文标题】:How to remove the nested loop join for large tables 【发布时间】:2021-12-29 07:51:19 【问题描述】:

SQL Server中有3张数据量很大的表,每张表大约有100000行。有一个 SQL 可以从三个表中获取行。它的性能很差。

WITH t1 AS 
(
    SELECT 
        LeadId, dbo.get_item_id(Log) AS ItemId, DateCreated AS PriceDate
    FROM 
        (SELECT 
             t.ID, t.LeadID, t.Log, t.DateCreated, f.AskingPrice
         FROM 
             t
         JOIN 
             f ON f.PKID = t.LeadID
         WHERE 
             t.Log LIKE '%xxx%') temp
)
SELECT COUNT(1)
FROM t1
JOIN s ON s.ItemID = t1.ItemId

在检查其估计的执行计划时,我发现它使用带有大行的嵌套循环连接。在下面的屏幕截图中掠夺。图像顶部返回 124277 行,底部执行 124277 次!我想这就是它这么慢的原因。

我们知道嵌套循环在处理大数据时存在很大的性能问题。怎么去掉,改用hash join或者其他join?

编辑:下面是相关函数。

CREATE FUNCTION [dbo].[get_item_Id](@message VARCHAR(200))
RETURNS VARCHAR(200) AS
BEGIN
    DECLARE @result VARCHAR(200),
            @index int

    --Sold in eBay (372827580038).
    SELECT @index = PatIndex('%([0-9]%)%', @message)
    IF(@index = 0)
     SELECT @result='';
    ELSE 
     SELECT @result= REPLACE(REPLACE(REPLACE(SUBSTRING(@message, PatIndex('%([0-9]%)%', @message),8000), '.', ''),'(',''),')','')
    -- Return the result of the function
    RETURN @result
END;

【问题讨论】:

你能把整个执行计划粘贴到brentozar.com/pastetheplan,这样我们也能看到整个事情吗?另外,请分享所涉及的表结构,以及您的表中存在的任何索引 使用一个函数会强制它去 RBAR。这是我要研究的第一件事。 嵌套循环运算符的警告是什么?我希望这表明 CROSS JOIN 但这在您发布的代码中似乎并不明显 @MartinSmith。这是 brentozar.com 上的执行计划。 brentozar.com/pastetheplan/?id=rkBrjsKiY我好想知道为什么是嵌套循环连接。 @AntonGrig - 不。即使根据它自己的估计,它最终也会将行数增加到 110 亿行,并产生一个成本为55,302 的计划,因此它不会因为估计错误而认为该计划很便宜,它只会生成一个非常低效的计划,大概是由于在连接条件中使用了非模式绑定 UDF 【参考方案1】:

由于某种原因,它决定执行s cross join t1,然后评估函数(结果别名为 Expr1002),然后对 [s].[ItemID]=[Expr1002] 执行过滤器(而不是执行 equi 连接)。

它估计它将有88,969124,277 行进入交叉连接(这意味着它将产生11,056,800,413

在交叉连接后执行标量 UDF 估计 110 亿次,然后过滤估计的 110 亿行似乎很疯狂。如果它在连接之前被评估,那么它的评估次数会少得多,而且也是一个 equi 连接,因此也可以使用 HASHMERGE 内部连接,并且只需读取所有表一次,而不会增加行数。

我在本地复制了这一点,并且在创建 UDF WITH SCHEMABINDING 时行为发生了变化 - SQL Server 将看到它不访问任何表并且它的定义是确定性的。

跟踪标志8606 输出似乎支持这个问题。 在这两种情况下,“简化树”阶段都将查询表示为与 ScalarUdf 上的谓词的交叉连接。标量 UDF 被注释为“IsDet”或“IsNonDet”,具体取决于函数是否是模式绑定的。在前一种情况下,“项目规范化”阶段在连接之前将计算推回,并给它一个在连接本身中引用的别名,在不确定的情况下,这不会发生。

我强烈建议摆脱这个标量函数并用内联版本替换它,因为除此之外非内联标量函数还有许多众所周知的额外性能问题。

新功能是

CREATE FUNCTION get_item_Id_inline (@message VARCHAR(200))
RETURNS TABLE
AS
    RETURN
      (SELECT item_Id = CASE
                          WHEN PatIndex('%([0-9]%)%', @message) = 0 THEN ''
                          ELSE REPLACE(REPLACE(REPLACE(SUBSTRING(@message, PatIndex('%([0-9]%)%', @message), 8000), '.', ''), '(', ''), ')', '')
                        END) 

并重写查询

WITH t1
     AS (SELECT t.LeadID,
                i.item_Id     AS ItemId,
                t.DateCreated AS PriceDate
         FROM   t
                CROSS apply dbo.get_item_Id_inline(t.Log) i
                JOIN f
                  ON f.PKID = t.LeadID
         WHERE  t.Log LIKE '%xxx%')
SELECT COUNT(1)
FROM   t1
       JOIN s
         ON s.ItemID = t1.ItemId 

可能仍有一些额外优化的空间,但这将比您当前的执行计划好几个数量级(因为那是灾难性的糟糕)。

【讨论】:

这是有道理的。谢谢。很有帮助!【参考方案2】:

要优化查询,请执行以下操作:

    将“t.Log LIKE 条件 '%xxx%'”用于更内部的选择。这样可以减少连接中包含的记录。 不要使用“喜欢”。 删除视图中的顶部选择。 优化“dbo.get_item_id”函数或使用替代解决方案,因为 此函数中的比较也非常耗时。

最后,您的查询将类似于以下代码:

WITH t1 AS
(
     SELECT 
          u.ID
        , u.LeadID as LeadId
        , dbo.get_item_id(u.Log) AS ItemId
        , u.DateCreated AS PriceDate
        , f.AskingPrice
    FROM 
    (select ID, LeadID, Log, DateCreated from t WHERE Log LIKE '%xxx%')u
    JOIN 
        f ON f.PKID = u.LeadID       
)
SELECT COUNT(1)
FROM t1
JOIN s ON s.ItemID = t1.ItemId'

【讨论】:

谢谢。我会看看你的建议并更新它。在我原来的帖子中,你知道它为什么使用嵌套循环吗? 这是由查询优化器决定的。对表数据进行排序或索引时会执行不同的行为。当索引在您的表上时,表中比较记录的数量似乎较少,并且选择了嵌套循环。您还可以对 Join 类型使用查询提示,但不建议这样做。【参考方案3】:

在一个大的结果上“计数”绝不是一个好主意。此外,您还有LIKE '%xxx%',它总是会导致完全扫描并且无法被优化引擎预测。

它知道,这是一种昂贵的方式,但我会重新设计应用程序。也许添加一些触发器并对数据结构进行反规范化可能是一个很好的解决方案。

【讨论】:

【参考方案4】:

如果您仍想使用 get_item_Id UDF。 这是它的高尔夫编码确定性版本。

CREATE FUNCTION [dbo].[get_item_Id](@message VARCHAR(200))
RETURNS VARCHAR(20)
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @str VARCHAR(20);
    SET @str = SUBSTRING(@message, PATINDEX('%([0-9]%',@message)+1, 20);
    IF @str NOT LIKE '[0-9]%[0-9])%' RETURN NULL;
    RETURN LEFT(@str, PATINDEX('%[0-9])%', @str));
END;

【讨论】:

以上是关于如何删除大型表的嵌套循环连接的主要内容,如果未能解决你的问题,请参考以下文章

python 如何对嵌套字典里的数据进行添加和删除?

如果在大 JSON 数据中发现类似的特定键,如何删除嵌套数据?

如何在 Postgres 中删除表的所有索引?

如何删除在 JavaScript 循环中连接字符串时添加的额外字符? [复制]

如何从循环中删除额外的 AND

mysql如何创建临时表