执行计划缓存不是性能问题根源,而是放大参数分布不均、统计信息滞后或调用方式不当的“显影剂”;真正需优化的是缓存背后的生成逻辑和复用条件,如查plan_generation_num确认重编译、禁用exec(@sql)、改用sp_executesql参数化、用局部变量缓解参数嗅探、统一set选项等。

执行计划缓存不是性能问题的根源,而是放大参数分布不均、统计信息滞后或调用方式不当的“显影剂”。真正要动的,是缓存背后的生成逻辑和复用条件。
查 plan_generation_num 确认是否真在重编译
别靠“感觉慢了”就怀疑缓存——得看 SQL Server 实际有没有反复丢弃旧计划。关键指标是 plan_generation_num:
- 查最近 10 次执行:运行
SELECT qs.plan_generation_num, qs.execution_count, st.text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st WHERE st.text LIKE '%YourProcName%' ORDER BY qs.last_execution_time DESC -
plan_generation_num > 1表示该计划被重建过;如果每次执行都涨 1,说明几乎没缓存住 - 配合
SET STATISTICS XML ON看执行计划里是否有 “Recompile” 节点,这是最直接证据
避免 EXEC(@sql) 彻底破坏缓存
动态拼接字符串后用 EXEC(@sql) 是缓存杀手,SQL Server 把每次生成的语句当全新 SQL 处理,根本不会进计划缓存。
- ✅ 正确写法:用
sp_executesql+ 参数化,例如EXEC sp_executesql N'SELECT * FROM Orders WHERE Status = @status', N'@status TINYINT', @status = 1 - ❌ 错误写法:
SET @sql = N'SELECT * FROM Orders WHERE Status = ' + CAST(@status AS VARCHAR); EXEC(@sql)—— 字符串拼接让语句文本每次都变 - 排序字段、表名、列名不能参数化,必须走
QUOTENAME()+ 白名单校验,否则既不安全也破坏缓存
用局部变量“断开”参数嗅探,比 OPTION(RECOMPILE) 更稳
OPTION (RECOMPILE) 看似一劳永逸,但代价是每次执行都重新编译,CPU 压力翻倍,高并发下反而拖垮整体吞吐。
- 更推荐做法:在存储过程开头声明同类型局部变量,把输入参数赋值过去,后续所有查询只用这个局部变量,例如
DECLARE @local_status TINYINT = @status; SELECT ... WHERE Status = @local_status - 这样优化器无法“嗅探”原始参数值,转而基于统计信息做平均估算,对极端值和常规值都能保持中等偏上的计划质量
-
OPTIMIZE FOR (@param = 'typical_value')适合已知典型值的场景,但一旦典型值迁移(比如新业务上线),又得改代码
SET 选项不一致会让相同逻辑产生多个缓存副本
两个一模一样的存储过程,一个开了 SET ARITHABORT ON,另一个没开,SQL Server 就认为它们是不同上下文,各自编译、各自缓存——内存白占,还增加编译开销。
- 检查当前会话默认 SET 选项:
SELECT @@OPTIONS,对比不同客户端连接的值(比如 SSMS 默认开ARITHABORT,而某些 ORM 不开) - 统一做法:在存储过程开头显式声明关键 SET 项,例如
SET ARITHABORT ON; SET NOCOUNT ON; -
SET NOCOUNT ON不仅防缓存分裂,还能减少网络往返消息量,尤其在循环或大批量操作中效果明显
真正难的不是加哪条 hint 或改哪行代码,而是判断“这次慢,到底是计划没复用,还是复用了错的计划”。前者看 plan_generation_num 和 execution_count,后者得结合 DBCC SHOW_STATISTICS 看数据分布、再用 SET STATISTICS IO/TIME 对比两次调用的物理读和编译时间——很多团队卡在这一步,直接跳到加 RECOMPILE,结果把 CPU 推到瓶颈。











