【问题标题】:Query performance hampered by seemingly light join看似轻量级的连接阻碍了查询性能
【发布时间】:2018-04-15 21:48:51
【问题描述】:

我对此查询的行为感到非常困惑(将其更改为实际查询):

   select * from fs_export_stage_data_customer m
    cross apply (select count(*) from fs_export_stage_data_customer m2 where m2.inscode=m.inscode) sq1(cnt)
    inner join fs_export_stage_session se on m.session=se.id 
    where cnt>1
    order by m.inscode,m.comb_dbupddate

按原样,它在

加入的执行计划: https://www.brentozar.com/pastetheplan/?id=Hk1cU3KRb

【问题讨论】:

  • 您是否使用了查询分析器或查看了执行计划,以确定它可能在哪里导致如此多的计算,以至于它会减慢如此大的速度?
  • 在此处粘贴您对内部联接查询的实际执行计划
  • 寻求性能帮助的问题应包括 DDL、DML 所涉及的表以及测试数据。如果您的测试数据很大,请尝试为表编写模式和统计信息(右键单击数据库->生成脚本->选择特定数据库对象->在下一个屏幕中选择高级并选择脚本统计信息)并将其粘贴到有问题的地方。使用此信息,任何人都可以重现您面临的相同问题。否则,回答您的问题变得非常困难。粘贴服务器版本也有帮助
  • 您的查询被低估了,它至少现在不应该使用嵌套循环和实际索引。哈希连接可以帮助您,但我们仍然希望您的实际计划采用 xml 格式
  • @GeorgeMenoutis 查询引用了执行计划屏幕截图中显示的不同表?此外,在两个查询的问题中都包含 SET STATISTICS IO ON 输出

标签: sql sql-server tsql inner-join


【解决方案1】:

正如我之前所说,您的查询被低估了。 可以这样纠正:

if object_id('tempdb.. #cnt') is not null drop table #cnt;

select inscode, count(*) as cnt
into #cnt
from fs_export_stage_data_customer
group by inscode
having count(*) > 1;


select * 
from fs_export_stage_data_customer m join #cnt c
        on m.inscode=c.inscode
    inner join fs_export_stage_session se 
        on m.session=se.id 
order by m.inscode,m.comb_dbupddate;

#cnt 基数是已知的,最终结果基数估计是正确的。

说明

这是您的执行计划,您可以在其中看到您的

(select count(*) from fs_export_stage_data_customer m2 where m2.inscode=m.inscode) sq1(cnt)

估计为 1 行,但有 33.092 行 count(*) > 1:

所以您的连接 fs_export_stage_data_customer m inner join fs_export_stage_session se on m.session=se.id 将有 33092 行,但服务器认为它只会产生 1 行。

然后它执行Nested Loops join 因为这个低估(NL 对少量行表现良好),但是我们做了:我们实现了由count(*) > 1 过滤的计数结果,所以服务器知道它会加入2 个表,其中一个表没有 1 行而是 33.092,因此它可以更改连接类型或至少不将 fs_export_stage_session 作为上表放在NL 中(上表应该比下表有更多的行)。

进一步阅读的链接:Nested Loops Join by Craig Freedman

您可以在同一博客中找到其他联接类型的描述。

保护 NL(来自上面引用的博客):

因为嵌套循环连接的复杂度与 外部输入的大小乘以内部输入的大小,a 嵌套循环连接通常 对于相对较小的输入表现最佳 套。内部输入不必很小,但是,如果它很大,它 有助于在高度选择性的连接键上包含索引。

【讨论】:

  • 6 秒,还不错!顺便说一句,您错过了 inscode 的群组。但是,您能否解释一下,或提供有关低估和基数的更多信息的链接?
  • 我更新了我的答案,添加了分组、解释和进一步阅读链接
  • 非常感谢您的丰富回答!
【解决方案2】:

变体:

在inscode上添加非聚集索引

添加 OPTION(FORCE ORDER) 这可以通过返回 HASH PLAN

您的估计和实际之间存在巨大差异:没有统计/指数

SELECT  
    m.session 
FROM data_customer m
INNER JOIN -- if no index on inscode may by try INNER HASH JOIN or create index
(
    SELECT inscode
    FROM data_customer 
    GROUP BY inscode
    HAVING COUNT(*) > 1
) as b
ON
    b.inscode = m.inscode
INNER JOIN 
    session se 
ON 
    m.session = se.id 
ORDER BY 
    m.inscode,
    m.comb_dbupddate

【讨论】:

  • 我已经试过了。还是很慢。不要不相信我们亲爱的十字架申请;)
  • 你从根本上忽略了db引擎的建议吗?
  • 什么意思?
  • 屏幕上的绿色文本 :) CREATE NONCLUSTERED INDEX ... inscode
  • 请注意,我的两个查询中都存在 inscode 上不存在索引,第一个查询以毫秒为单位运行。那么,这不是问题所在吗?
【解决方案3】:

也许你可以试试这个。先得到结果再做join。

WITH Cte
     AS (
     SELECT M.Session
     FROM Data_Customer AS M
       CROSS APPLY
       (
           SELECT COUNT(*)
           FROM Data_Customer
           WHERE Inscode = M.Inscode
                                    ) AS Sq1(Cnt)
     WHERE Cnt > 1
     ORDER BY M.Inscode,
          M.Comb_Dbupddate)
     SELECT *
     FROM Cte AS C
       INNER JOIN Session AS Se ON C.Session = Se.Id;   

【讨论】:

  • 删除 cte 中的订单后,我得说它的运行速度和我的版本一样慢。
  • 这是因为任何 CTE 在执行之前都会被扩展,我敢打赌原始查询的执行计划和这个带有 CTE 的执行计划是相同的
猜你喜欢
  • 1970-01-01
  • 2011-06-08
  • 1970-01-01
  • 1970-01-01
  • 2019-09-25
  • 1970-01-01
  • 2021-10-17
  • 1970-01-01
  • 2021-12-16
相关资源
最近更新 更多