参数嗅探一直是 SQL Server 存储过程里最容易引发“同一条 SQL,时快时慢”的问题之一。到了 SQL Server 2024,参数敏感计划(PSP)开始能为同一查询保留多种执行计划,但前提并不宽松。想真正把这个能力用起来,关键不在于记住一个开关,而是先判断查询是否满足触发条件,再决定该用 PSP、局部变量,还是只对个别慢语句加 OPTION (RECOMPILE)。
怎么确认查询是否真正启用了参数敏感计划
不是所有带参数的查询都能自动获得多套执行计划。SQL Server 2024 中,PSP 只有在满足一组前提时才会介入:

- 列统计信息的直方图必须体现非均匀分布。例如某字段中 95% 的值是
'A',其余 5% 才分布在'B'到'Z',这类明显倾斜的数据更容易触发 PSP。 - 优化器最多只会对 前三个谓词做 PSP 评估。也就是说,
WHERE里即便有多个@param = col条件,真正参与判断的通常也只是最敏感的三个。 - 必须生成完全优化的计划,也就是执行计划中能看到
StatementOptmLevel="FULL"。如果语句用了OPTION (RECOMPILE),或者属于分布式查询,就不参与 PSP。 - 查询存储必须启用,且数据库的
QUERY_STORE需要处于READ_WRITE模式。 - 数据库兼容级别还要满足
>= 160,否则 PSP 根本不会启用。
确认方式不要只看“理论上支持”,最好直接看执行结果。执行目标慢查询后,可以先查 sys.query_store_plan,观察 is_forced_plan 和 query_plan_hash 是否出现多个不同哈希值;再结合 sys.dm_exec_query_stats,检查是否存在相同 sql_handle、但对应多个不同 plan_handle 的记录。
如果同一条参数化查询始终只有一套计划,基本就说明 PSP 没有接管,后面就需要继续排查触发条件是否被卡住。
为什么开了 PSP 还是只有单计划
很多人以为升级版本、打开功能后,参数嗅探问题就会自动缓解,实际并不是这样。PSP 失效往往发生在几个很具体的地方。
参数定义不一致,会直接破坏计划比较基础
如果同一个 @param 每次调用时类型或长度不一致,例如一次传 nvarchar(10),另一次传 nvarchar(50),计划缓存就可能被拆散。这样一来,优化器拿不到稳定的比较对象,PSP 很难对同一查询形成多变体计划。
谓词里混用参数和字面量,可能让 PSP 识别失败
像下面这种写法就要小心:

WHERE Company = @Company AND Status = 'Active'当条件中一部分是参数、一部分是固定字面量时,谓词独立性会变差,PSP 未必能按预期识别出真正的参数敏感点。问题不一定每次都暴露,但在复杂查询里,这类写法确实容易降低可预测性。
统计信息不准,优化器就无法判断值分布
PSP 依赖统计信息去区分“哪些参数值会返回很少数据,哪些会触发大结果集”。如果 UPDATE STATISTICS 没有及时运行,或者采样率太低,直方图失真后,优化器就很难做出多计划分流。
这时可以直接在 SSMS 里执行:
DBCC SHOW_STATISTICS('YourTable', 'YourIndex')重点看 Histogram 是否足够细,通常可以关注步骤数是否达到 >= 200,以及 AVG_RANGE_ROWS 是否存在明显跳变,例如从 1 直接跳到 10000。这类差异越明显,越说明数据分布倾斜,PSP 才更有发挥空间。
兼容级别不够,功能根本不会工作
如果数据库兼容级别低于 160,即便 SQL Server 版本已经更新,也不能指望 PSP 正常生效。这是最基础、也最容易被忽略的一项检查。
局部变量怎么和 PSP 配合,才能更稳地压住嗅探问题
只依赖 PSP 往往有盲区,尤其是在参数模式复杂、调用方式不一致的存储过程里。更稳妥的思路,是用局部变量先把关键参数接住,再让优化器基于更稳定的表达式去生成计划。
这里有几个细节不能写错:
- 声明和赋值最好放在同一行,例如:
DECLARE @LocalCompany NVARCHAR(100) = @Company;。如果拆成DECLARE再SET,有些场景下可能被优化器绕过,起不到预期隔离效果。 - 类型和长度必须严格一致。若
@Company是NVARCHAR(50),局部变量就不要写成NVARCHAR(20),否则隐式转换会影响索引使用,最后问题从参数嗅探变成索引失效。 - 只要是关键过滤条件,就要一起处理。假设
WHERE里既有@Company又有@Region,那两个都应该映射到局部变量,漏掉一个,嗅探仍可能从剩余参数穿透进来。 - 局部变量并不会天然阻止 PSP。它只是把“直接基于输入参数编译”的不确定性,转移到局部变量层,PSP 仍可能基于
@LocalCompany这样的运行时值去选择计划变体。
更稳妥的结构可以写成这样:
CREATE PROCEDURE sp_get_sales
@Company NVARCHAR(100),
@Region CHAR(2)
AS
BEGIN
DECLARE @LocalCompany NVARCHAR(100) = @Company;
DECLARE @LocalRegion CHAR(2) = @Region;
SELECT SUM(SalesAmount)
FROM SalesTable
WHERE Company = @LocalCompany
AND Region = @LocalRegion
OPTION (OPTIMIZE FOR (@LocalCompany UNKNOWN)); -- 可选:给 PSP 加一层提示
END这里的 OPTION (OPTIMIZE FOR (@LocalCompany UNKNOWN)) 不是必选项,但在某些分布不够稳定的场景下,可以作为额外提示,帮助优化器避免过度依赖某一次参数值。
OPTION (RECOMPILE) 还要不要用,应该加在哪里
到了 2024 年,PSP 并没有让 OPTION (RECOMPILE) 过时,只是把它的使用范围压缩到了更明确的场景里。
只给具体慢语句加,不要给整个过程加
正确思路是把重编译限制在真正有问题的那条语句末尾,例如:
SELECT ... WHERE id = @x OPTION (RECOMPILE);这样做的目标很明确:只为这一句重新编译,不把整个存储过程都拖下水。
适合低频、跨度极大的参数场景
如果参数波动非常夸张,但执行频率并不高,OPTION (RECOMPILE) 仍然是更直接的办法。典型例子就是分页:
OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY当 @skip 从 10 跳到 1000000 时,单一计划往往很难兼顾,临时重编译反而更容易拿到合适方案。
别只重编译子查询,漏掉主 DML
在 INSERT、UPDATE 这类语句里,有些人会把 OPTION (RECOMPILE) 加在内部子查询上,却没有处理主语句本身。结果就是子查询重编译了,外层 DML 仍旧沿用旧计划,性能改善非常有限。
避免用 WITH RECOMPILE 创建整个过程
不要轻易用 WITH RECOMPILE 来创建存储过程。这样会让整个过程每次执行都重编译,CPU 开销通常比单条语句重编译高得多,代价并不小。
还要注意一个容易被忽略的事实:PSP 和 OPTION (RECOMPILE) 是互斥的。只要加了后者,前者就不会生效,因此不要把两者叠在同一条语句上。
混合场景下怎么判断该拆语句还是换策略
实际最棘手的,并不是“PSP 和 RECOMPILE 选哪个”这么简单,而是一个存储过程中常常同时存在两类完全不同的查询。
例如,一部分语句是高频、小结果集查询,更适合依赖 PSP 维护多套计划;另一部分语句则是低频、可能触发大范围扫描的重型查询,这时反而更适合单独使用 OPTION (RECOMPILE)。如果把它们硬塞进同一策略里,通常谁都照顾不好。
更可执行的做法,是把这些语句拆开看待,按语句级别决定策略,而不是给整个存储过程贴一个统一标签。判断依据也不复杂,先监控 sys.dm_exec_query_stats 中各语句的 last_logical_reads 和 last_elapsed_time,把真正波动大的语句筛出来,再决定是保留 PSP、改写为局部变量模式,还是对个别语句直接重编译。
换句话说,参数嗅探优化从来不是“选一个银弹”就结束。SQL Server 2024 的 PSP 确实让问题比过去更容易处理,但它仍然高度依赖统计信息、兼容级别、参数写法和语句结构。对 SQL Server 2022/2024 环境来说,最稳妥的路线仍是先验证 PSP 是否真的生效,再按语句粒度补上局部变量或 OPTION (RECOMPILE),而不是把希望押在某一个开关上。







