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

L. *_*ler 13 performance sql-server execution-plan sql-server-2014 query-performance

我有一个查询,其中使用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
Run Code Online (Sandbox Code Playgroud)

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

这是生成的查询计划:

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

当我更改c.ID为 时*,查询以 107,049 次逻辑读取结束,在所有三个表之间相当均匀地分布。它使用了 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没有提示的版本相比,使用提示确实大大减少了读取次数,但它仍然是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。总体而言,它们比更新前更接近,但计划仍然不同。

在 2016 年以 2014 年兼容模式和 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

我注意到在执行排序的大索引查找之后运算符的执行计数在单线程版本中仅执行 1,000 次,但在并行版本中执行次数明显更多,在各种运算符的 2,622 到 4,315 次执行之间。

Joe*_*ish 5

确实,选择更多列意味着 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 *查询所需的较大行大小将优化器推向循环连接计划而不是散列连接计划。由于基数估计问题,循环连接计划的成本高于应有的成本。通过仅选择一列来减少行大小大大降低了散列连接计划的成本,但可能不会对循环连接计划的成本产生太大影响,因此您最终会得到效率较低的散列连接计划。对于匿名计划,很难说更多。