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

How to remove the nested loop join for large tables

SQL服务器中有3个table数据量大,每个table大约有100000行。有一个 SQL 从三个 table 中获取行。它的性能很差。

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

在检查其估计的执行计划时,我发现它使用了大行的嵌套循环连接。抢劫下面的截图。图像中的顶部 return 124277 行,底部执行了 124277 次!我想这就是它这么慢的原因。

我们知道嵌套循环对于大数据有很大的性能问题。如何删除它,并改用散列连接或其他连接?

编辑:下面是相关函数。

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;

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

  1. 将“t.Log LIKE 条件'%xxx%'”取到更内部的选择。这允许在连接中包含更少的记录。
  2. 不要使用“喜欢”。
  3. 删除您视图中的首选。
  4. 优化“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'

“COUNT”个大结果从来都不是一个好主意。此外,您还有 LIKE '%xxx%',它总是会导致全面扫描,优化引擎无法预测。

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

出于某种原因,它决定执行 s cross join t1 然后评估函数(结果别名为 Expr1002),然后对 [s].[ItemID]=[Expr1002] 进行筛选(而不是执行equi 加入)。

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

在交叉连接后执行标量 UDF 估计 110 亿次,然后向下过滤估计的 110 亿行确实看起来很疯狂。如果在连接之前对其进行评估,它将被评估的次数少得多并且也将是一个等值连接,因此也可以使用 HASHMERGE 内部连接并且只读取所有表一次而不会增加行数.

我在本地复制了这个,当创建 UDF 时行为发生了变化 WITH SCHEMABINDING - SQL 然后服务器会发现它不访问任何表并且它的定义是确定性的。

跟踪标志 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 

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

以防您仍想使用 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;