我观察(并重现)了 SQL Server 2022 中的以下场景。
使用中的模式
- 代码通过 sp_executesql 执行(不涉及存储过程)
- 第一个查询将数据选择到临时表中
- 然后,DDL 语句会在临时表上创建聚集索引。临时表绝对不可缓存——首先,这不是一个模块(存储过程或函数),而且我们在临时表填充后创建索引。因此,我不希望缓存的临时对象上留下的统计信息在这里被涉及。
- 查询从临时表中选择数据。此查询每次都会获得完全优化(不是 TRIVIAL 计划)
该批处理可以针对小型和大型数据集运行,因此临时表中可以包含 1 行或数千行。
此行为通常发生在可读辅助数据库上。没有可写查询存储,也没有自动计划强制因素。
我已经验证我也可以针对主副本重现该行为。(自动计划更正被告知忽略查询,并且我确认重现时没有在主副本上强制执行计划。)
复制脚本
- 安装脚本- 我在 SQL Server 2022 CU15 上运行了此脚本。这会关闭查询存储并使用兼容级别 130。
- 重现查询- 我一直通过 SQL Query Stress 运行该查询,因此我可以轻松地在一个或多个线程上同时运行该查询
- 计划生成编号和临时表- 一个非常简单的查询,用于观察系统查询统计中的 plan_generation_num(“重新编译后可用于区分计划实例的序列号。”)和当前的临时表列表
通常会发生什么——以及我期望的行为
通常,在查询执行之间更改临时表中的大量行会自动导致重新编译,并且我会看到从临时表中选择数据的查询具有与临时表中的行匹配的行估计值。
当它按预期工作时,性能良好。
使用重现查询:如果我清除计划缓存,然后在 SQL 查询压力中的单个线程上运行重现查询 40 次迭代,plan_generation_number 最终为 82。当使用 sp_WhoIsActive 对查询计划进行采样时,查询临时表的行估计值与临时表中的行数匹配,正如预期的那样。
有时会发生的情况——在我看来像是一个错误
在极少数情况下,我看到正在使用的计划中,临时表的估计计划只有 1 行,但临时表中实际上有大量行。很多行已更改,但它没有自动重新编译:
这会导致性能非常缓慢,因为低估计计划决定使用没有预取的嵌套循环,这使其成为 CPU 消耗器。
使用重现查询:如果我清除计划缓存,然后在 SQL Query Stress 中的 2 个线程上运行重现查询 20 次迭代,则 plan_generation_number 最终会小于 82——它因运行而异,但可能是 72 或 59,表示重新编译次数较少。在运行时,我还可以使用 sp_WhoIsActive 对估计的行数为单个但临时表中的行数更多的情况进行采样。屏幕截图:
我只能在多个并发会话上运行重现代码时重现此问题
我无法在 SQL Server 中使用单个会话重现此行为。我能重现此行为的唯一方法是设置一个代码块:
- 执行至少 1 次 sp_executesql 语句的迭代,该语句在临时表中有 1 行
- 然后执行 sp_executesql 语句的 1 次迭代,该语句在临时表中包含更多行
如果我在单个会话中运行此程序,则无法重现问题。但如果我在四五个会话中同时运行此程序,则偶尔会出现“未按应有的方式重新编译”的问题。(注意:使用 SQL Query Stress,我只需 2 个会话/迭代即可重现此问题。)
我觉得这像是一个 bug,我很好奇是否有人见过它。不过,使用临时表重新编译和统计行为非常复杂,因此我可能忽略了它如何与不可缓存的临时表一起工作的一些细微差别。
PS:我确实认为可缓存的临时表通常更好。我只是想弄清楚为什么这种行为会在此时在不可缓存的临时表场景中发生。
解决方法
在查询中添加后option (recompile)
,我无法再重现查询临时表的 1 行计划的重用。这已经足够了,但我很困惑为什么这是必要的。