以下简单查询在最近几天多次超时:
SELECT Object1.*,
Object2.Column1,
Object2.Column2 AS Column3
FROM Object2 INNER JOIN Object1 ON Object2.Column2 = Object1.Column3
WHERE Object2.Column4=Variable1 AND
Object2.Column5=Variable2 AND
Object1.Column6=Variable3
我可以在 SentryOne 中捕获它并看到它频繁执行,运行到应用程序设置的 60 秒查询超时并不断导致 12 次 Mio 读取。
我看不到任何相关的东西,比如导致超时的阻塞或死锁。
我复制了查询并在 SSMS 中运行它。它在几毫秒内返回并返回零行。这是我得到的执行计划:https ://www.brentozar.com/pastetheplan/?id=SJ-LK8jug
Lateron,我用相同的查询和相同的参数值再次执行了这一步。突然它运行了大约 90 秒返回零行,我得到了一个不同的计划如下:https ://www.brentozar.com/pastetheplan/?id=HyVu58i_l
如您所见,估计行数为 1,实际行数巨大。这让我猜测对表格进行了很多更改。因此,我查看了所涉及表的 [sys].[dm_db_stats_properties] 值,尤其是 OBJECT1 和使用的索引。
[请注意,为避免混淆,匿名计划对不同的索引使用相同的名称 (Object1.Index1)]
在这一点上,我看到了以下统计值......
Object1.Index1(引用第二个低效的执行计划):
RowsInTable=3826101
RowsSampled=103245
UnfilteredRows=3826101
RowMods=2140
Histogram Steps 200
PercentChanged=0.0
对于 Object1.Index2(聚集索引):
RowsInTable=3826101
RowsSampled=103734
UnfilteredRows=3826101
RowMods=2140
HistoSteps=199
PercentChanged=0.0
然后我意识到我在第一次执行时不小心添加了一个换行符,我认为这让我有了一个不同的、新的执行计划。
我决定更新 OBJECT1 表的所有统计信息。之后我再次运行初始查询,因为我从 SentryOne 中捕获了它,没有任何更改,没有换行符......
这次如预期的那样快,执行计划与第一个有效计划相同。这让我怀疑统计数据有点陈旧。
我再次查询统计元信息,结果如下(参考第一个高效计划):
Object1.Index1(聚集索引)
RowsinTable=3828157
RowsSampled=104017
UnfilteredRows=3828157
RowModifications=14
HistoSteps=199
PercentCahnge=0.0
对于 Object1.Index2(非聚集索引)
RowsInTable=3828157
RowsSampled=103275
UnfilteredRows=3828157
RowMods=14
HistogrSteps=127
PercentChanged=0.0
应用程序随后按预期运行,速度很快,没有超时。所以我想 STATISTICS UPDATE 在这里有所帮助。
让我另外指出,作为我的自动夜间索引和统计维护的一部分,表格的所有索引已在昨晚成功维护/更新。
现在我的问题:
我知道如果执行计划期望的行数很少并且实际上返回的行数比预期多很多,那么执行计划就会有问题。我不明白如果执行计划实际上返回零行,它如何显示 3,141,000 行。这怎么可能?
对表 OBJECT 1 及其统计数据的调查没有显示任何有关较大更改或添加行的提示。自上次自动索引和统计维护以来,我查询了添加或更改的行,看起来有 2370 行被更改,而表中有 ~ 3,800,000 行。由于 [sys].[dm_db_stats_properties] 的值也显示了这一点,因此发生了轻微的变化。统计数据真的是一个问题吗?我上面引用的数字是否显示了更新统计数据的充分理由?
更新: ParameterCompiledValue 和 ParameterRuntimeValue 的值在好计划中是相同的,但在坏计划中是不同的。表 OBJECT1 在 Column6 中有一个值提供了 > 3 Mio 行,而所有其他值提供了最多约 60k 行。BAD 计划使用 ParameterRuntimeValue 的 thi >3 Mio Rows 值,而它是使用仅提供 160 行的值编译的。所以看起来我需要一个解决这两种情况的计划,或者一个更灵活的解决方案来创建一个适当的计划......?
关于您的第一个问题:
我不明白如果执行计划实际上返回零行,它如何显示 3,141,000 行。这怎么可能?
优化器在生成计划时不知道最终输出行数。所以它所能考虑的只是它可以从统计数据中计算出的估计值。(在你的“坏”计划的情况下,估计的输出行实际上是 4.4,这是基于计划中的第一个估计。)
如果这些估计已过时或不够准确(例如,不均匀分布数据的样本与全扫描),那么即使使用简单的查询也可能生成糟糕的计划。
此外,如果一个计划与不同的变量一起重复使用,则生成该计划的估计可能会非常不准确。(而且我认为这就是 sp_BlitzErik 在您的特定情况下倾向于作为原因的原因。)
更新:
您的最新更新表明您看到的问题是由典型的不适当的参数嗅探引起的。
最简单的解决方案(如果您控制代码)是将 OPTION (RECOPILE) 添加到有问题的查询的末尾。这将确保在每次运行时都重新创建语句计划,并且还允许在创建计划时采取某些捷径。
缺点是每次运行都会花费额外的 CPU 和时间来创建计划,因此此解决方案可能不合适。
考虑到数据的偏差(一个值 3 mill vs 其他值 160k max),并假设偏差不会有太大变化,这样的分支可能会起作用:
请注意,有两个地方的“3MillValue”需要使用返回该数量的值进行硬编码,而 OPTIMIZE FOR 是此技术的关键。