为什么选择此查询的所有结果列比选择我关心的一列要快?


13

我有一个查询,其中使用select *不仅读取次数少得多,而且比使用占用的CPU时间少得多select c.Foo

这是查询:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

这完成了2,473,658个逻辑读取,大部分在表B中。它使用了26,562个CPU,持续时间为7,965。

这是生成的查询计划:

选择单个列的值进行计划 在PasteThePlan上:https ://www.brentozar.com/pastetheplan/ ? id = BJAp2mQIQ

当我改变c.ID*,查询与完成107049逻辑读,非常均匀地分布所有三个表之间。它使用4,266 CPU,持续时间为1,147。

这是生成的查询计划:

选择所有值进行计划 在PasteThePlan上:https ://www.brentozar.com/pastetheplan/ ? id = SyZYn7QUQ

我试图用乔Obbish建议的查询提示,与这些结果:
select c.ID无提示:https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID与提示:https://www.brentozar.com/pastetheplan/ ?id = B1W ___ N87
select *无提示:https : //www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select *有提示:https : //www.brentozar.com/pastetheplan/?id=rJhhudNIQ

OPTION(LOOP JOIN)select c.ID没有提示的版本相比,使用带有with 的提示确实极大地减少了读取次数,但仍比select *没有任何提示的查询读取次数多了约4倍。添加OPTION(RECOMPILE, HASH JOIN)select *查询中使其性能比我尝试过的任何其他方法都要差得多。

在使用更新表及其索引的统计信息之后WITH FULLSCANselect c.ID查询的运行速度更快:
select c.ID更新之前:https : //www.brentozar.com/pastetheplan/?id=
select * SkiYoOEUm更新之前:https : //www.brentozar.com/ ?pastetheplan / ID = ryrvodEUX
select c.ID更新后:https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select *更新后:https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *select c.ID在总持续时间和总读取次数方面仍然优于(select *大约是读取次数的一半),但是它确实使用更多的CPU。总体而言,它们比更新之前要紧密得多,但是计划仍然不同。

在以2014 Compatibility模式运行的2016和2014上可以看到相同的行为。什么可以解释这两个计划之间的差异?可能是因为尚未创建“正确的”索引吗?统计信息可能会稍微过时导致此吗?

我尝试ON以多种方式将谓词上移到连接的一部分,但是每次查询计划都是相同的。

重建索引后

我在查询所涉及的三个表上重建了所有索引。 c.ID仍然执行最多的读取(是的两倍*),但是CPU使用率约为该*版本的一半。该c.ID版本还蔓延到tempdb数据库上的排序ATable
c.IDhttps://www.brentozar.com/pastetheplan/?id=HyHIeDO87
*https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

我还尝试强制它在没有并行性的情况下运行,这给了我最佳性能的查询:https : //www.brentozar.com/pastetheplan/?id=SJn9-vuLX

我注意到在执行大索引查找之后,在单线程版本中仅执行了1000次排序的操作符的执行计数,但是在并行版本中执行的排序却要多得多,介于各种操作符的2,622至4,315次执行之间。

Answers:


4

的确,选择更多的列意味着SQL Server可能需要更努力地工作才能获得请求的查询结果。如果查询优化器能够针对两个查询提出完美的查询计划,则可以合理地预期SELECT *查询的运行时间比从所有表中选择所有列的查询要长。您在一对查询中观察到相反的情况。在比较成本时需要小心,但是慢速查询的总估计成本为1090.08优化器单元,而快速查询的总估计成本为6823.11优化器单元。在这种情况下,可以说优化器在估算总查询成本方面做得不好。它确实为您的SELECT *查询选择了一个不同的计划,并且期望该计划会更昂贵,但事实并非如此。这种类型的不匹配可能由于多种原因而发生,最常见的原因之一是基数估计问题。运营商成本很大程度上取决于基数估计。如果计划关键点的基数估计不准确,则计划的总成本可能无法反映现实。这是一个过分的简化,但是我希望这将有助于理解这里发生的情况。

让我们开始讨论为什么SELECT *查询可能比选择单个列更昂贵。该SELECT *查询可能会将一些覆盖索引变成非覆盖索引,这可能意味着优化器需要做额外的工作才能获得它需要的所有列,或者可能需要从更大的索引中读取。SELECT *可能还会导致更大的中间结果集,需要在查询执行期间进行处理。通过查看两个查询中的估计行大小,可以看到实际的效果。在快速查询中,行大小范围为664字节至3019字节。在慢速查询中,您的行大小范围为19到36个字节。诸如排序或哈希生成之类的阻塞运算符将对具有较大行大小的数据产生更高的成本,因为SQL Server知道对大量数据进行排序或将其转换为哈希表的成本更高。

通过快速查询,优化器估计它需要对240万个索引进行搜索Database1.Schema1.Object5.Index3。那就是大部分计划成本的来源。然而实际计划显示,该操作员仅执行了1332次索引查找。如果将这些循环联接的外部的实际行与估计行进行比较,则会发现差异很大。优化器认为,将需要更多的索引查找来找到查询结果所需的前1000行。这就是为什么查询具有相对较高的成本计划但完成速度如此之快的原因:被认为是最昂贵的运算符的执行量不到预期工作量的0.1%。

查看慢速查询,您会得到一个主要包含哈希联接的计划(我相信循环联接仅用于处理局部变量)。基数估计绝对不是完美的,但是唯一真正的估计问题恰好在排序的最后。我怀疑大部分时间都花在扫描具有数亿行的表上。

您可能会发现将查询提示添加到两个版本的查询以强制与另一个版本关联的查询计划会有所帮助。查询提示可能是弄清楚优化器为何做出某些选择的好工具。如果添加OPTION (RECOMPILE, HASH JOIN)SELECT *查询中,我希望您会看到与哈希联接查询类似的查询计划。我还希望哈希联接计划的查询成本会更高,因为您的行大小会更大。因此,这可能就是为什么没有为SELECT *查询选择哈希联接查询的原因。如果您将OPTION (LOOP JOIN)查询添加到仅选择一列的查询中,那么我希望您会看到与SELECT *查询。在这种情况下,减小行大小不会对整体查询成本产生太大影响。您可能会跳过关键查找,但这只占估计成本的一小部分。

总而言之,我希望满足SELECT *查询所需的较大行大小会将优化器推向循环联接计划,而不是哈希联接计划。由于基数估计问题,循环连接计划的成本高于应有的成本。通过仅选择一列来减小行大小可大大降低哈希联接计划的成本,但可能不会对循环联接计划的成本产生太大影响,因此最终会导致效率较低的哈希联接计划。对于匿名计划,很难说的比这更多。


非常感谢您的广泛和翔实的回答。我尝试添加您建议的提示。它确实使select c.ID查询快得多,但它仍在做一些select *没有提示的查询所要做的额外工作。
L. Miller

2

统计数据过时肯定会导致优化器选择不良的数据查找方法。您是否尝试过对索引进行“ UPDATE STATISTICS ... WITH FULLSCANREBUILD”运算?尝试一下,看看是否有帮助。

更新

根据OP的更新:

使用更新了表及其索引的统计信息后WITH FULLSCANselect c.ID查询的运行速度大大提高

因此,现在,如果唯一采取的措施是UPDATE STATISTICS,则尝试做一个索引REBUILD(不是REORGANIZE),因为我发现这有助于估计行计数,UPDATE STATISTICS而行和索引都REORGANIZE没有。


我能够在周末重新获得涉及的三个表上的所有索引,并更新了我的帖子以反映这些结果。
米勒

-1
  1. 您能否包括索引脚本?
  2. 您是否消除了“参数嗅探”的可能问题?https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. 我发现这种技术在某些情况下很有用:
    a)按照以下规则将每个表重写为子查询:
    b)SELECT-将联接列放在首位
    c)PREDICATES-移至其各自的子查询
    d)ORDER BY-移至其子查询各自的子查询,首先在JOIN COLUMNS上排序
    e)为最终排序和SELECT添加包装查询。

这个想法是对每个子选择中的联接列进行预排序,将联接列放在每个选择列表的第一位。

这就是我的意思。

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1.我将尝试在原始文章中添加索引DDL脚本,这可能需要一段时间才能“清理”它们。2.我通过在运行前清除计划缓存以及用实际值替换bind参数来测试了这种可能性。3.我尝试过此操作,但是ORDER BY在没有TOP,FORXML等的子查询中无效ORDER BY
米勒
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.