执行计划改变是优化器对环境变化的正常响应,而非bug;参数嗅探、统计信息过期、set选项不一致、动态sql写法不当等均会导致计划劣化。

执行计划改变不是“出 bug”,而是 SQL Server 或 Oracle 在按规则响应环境变化——参数值、统计信息、SET 选项、索引结构这些只要有一个变了,优化器就可能换计划。
参数嗅探(Parameter Sniffing)让首次值决定后续命运
SQL Server 编译存储过程时会“嗅探”第一次传入的 @status 值,并据此生成计划;如果首次是 @status = 'archived'(仅 0.1% 行),它大概率选索引查找 + RID Lookup;之后调用 @status = 'active'(95% 行)仍复用该计划,逻辑读暴增。
- 验证方法:查
sys.dm_exec_query_stats中同一sql_handle的多次执行,对比total_logical_reads和execution_count是否剧烈波动 - 典型信号:
EstimateRows在执行计划 XML 中远小于实际扫描行数(比如预估 1 行,实际读 40 万页) - 别一上来就加
OPTION (RECOMPILE)——高并发下 CPU 会扛不住
统计信息过期或更新不完整直接误导优化器
优化器靠统计信息估算行数,一旦数据量翻倍、新增覆盖索引、或删了低效索引,旧计划就可能失效。SQL Server 2022 默认自动更新阈值是“20% 行变更 + 500 行”,对千万级表基本不起作用。
- 手动触发更准:
UPDATE STATISTICS table_name WITH FULLSCAN - 检查碎片:
sys.dm_db_index_physical_stats中avg_fragmentation_in_percent > 30的索引建议REBUILD - 注意:
REORGANIZE不更新统计信息,REBUILD会自动更新
SET 选项不一致导致计划“互相不认识”
哪怕两个连接执行完全一样的存储过程,只要一个开了 SET ARITHABORT ON、另一个没开,SQL Server 就认为它们是不同上下文,各自编译、各自缓存——结果就是 usecounts = 1,看似快了,实则每次都在硬编译。
- 查当前会话:
SELECT SESSIONPROPERTY('ARITHABORT')(返回 1=ON,0=OFF) - ORM(如 Entity Framework)默认发
SET ARITHABORT ON,而 SSMS 默认关着——这就是为什么 SSMS 测得快、上线就慢 - 避免在过程中动态改 SET:
SET ANSI_NULLS OFF后再查表,会强制整条语句重编译
动态 SQL 写法彻底绕过计划缓存
用 EXEC(@sql) 拼接字符串,SQL Server 把每次生成的语句当全新 SQL 处理,根本不会进缓存;哪怕只差一个空格,哈希值就变,缓存形同虚设。
- ✅ 正确写法:
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()+ 白名单校验,否则既不安全也破坏缓存
真正难处理的不是某一次计划变差,而是多个因素叠加:比如统计信息刚更新完、恰好又来了个极端参数、客户端还带着不同的 SET 选项——这种组合会让问题极难复现,也最容易被归因为“数据库抽风”。盯住 plan_generation_num 和 sys.dm_exec_query_stats 的波动,比猜原因更可靠。











