参数嗅探不是 bug,是 SQL Server 的默认行为;90% 场景用局部变量法最稳,高频低延迟查询才考虑 OPTION (RECOMPILE)。

直接结论:参数嗅探不是 bug,是 SQL Server 的默认行为;解决它不靠“禁用”,而要选对策略——90% 的场景用局部变量法最稳,高频低延迟查询才考虑 OPTION (RECOMPILE)。
为什么存储过程第一次快、后面突然变慢?
SQL Server 在首次执行存储过程时,会“记住”你传入的参数值(比如 @Company = 'SmallCo'),并基于这个值对应的数据分布生成执行计划(例如走聚集索引扫描)。该计划被缓存后,后续哪怕传入 @Company = 'LargeCo'(数据量大 100 倍),也照搬原计划——结果就是大量随机 I/O 或内存溢出,查询从 200ms 拉长到 12s。
常见错误现象包括:
- 同一存储过程,不同参数下执行时间波动剧烈(如 0.3s vs 8.7s)
-
DBCC FREEPROCCACHE后第一次又快,过一阵又慢 - SSMS 中直接写
WHERE Company = 'LargeCo'很快,但用参数@Company就慢
局部变量法:最常用、低风险、零编译开销
原理是让优化器在编译阶段“看不到”真实参数值,从而放弃参数嗅探,转而基于列统计信息的平均密度生成通用计划。它不强制重编译,也不依赖典型值假设,适合绝大多数 OLTP 场景。
实操建议:
- 在存储过程开头声明同类型局部变量,并立即赋值:
DECLARE @LocalCompany NVARCHAR(100) = @Company; - 后续所有 WHERE / JOIN 条件中只使用该局部变量,**不要混用**原始参数
- 注意类型必须严格一致(比如
NVARCHAR(100)不能赋给VARCHAR(100),否则可能触发隐式转换+新计划缓存) - 不适用于需要精确基数估算的报表类查询(如分组聚合后取 TOP 10)
示例片段:
CREATE PROCEDURE GetSalesByCompany @Company NVARCHAR(100)
AS
BEGIN
DECLARE @LocalCompany NVARCHAR(100) = @Company;
SELECT SUM(SalesAmount)
FROM SalesTable
WHERE Company = @LocalCompany;
END
OPTION (RECOMPILE):精准但有代价,慎用于高并发
每次执行都丢弃旧计划、重新编译,确保计划完全适配当前参数。适合参数差异极大、且单次执行耗时远高于编译开销(通常 > 50ms)的场景,比如后台导出、复杂报表。
关键限制与风险:
- 每秒执行超 50 次的存储过程,CPU 编译压力会明显上升
- 无法被计划向导(
sp_create_plan_guide)捕获或固定 - 不能加在存储过程定义头(如
WITH RECOMPILE是整过程重编译,粒度太粗),应只加在具体 SELECT/UPDATE 语句末尾 - 若语句含多个查询,只对加了提示的那一条生效
正确写法:
SELECT SUM(SalesAmount) FROM SalesTable WHERE Company = @Company AND Account = @Account OPTION (RECOMPILE);
别踩这些坑:OPTIMIZE FOR 和动态 SQL 的误用
OPTIMIZE FOR (@p = 'TypicalValue') 看似聪明,但业务一变就失效;而 sp_executesql 或拼接字符串看似绕过问题,实则引入注入风险和计划碎片化。
更隐蔽的陷阱:
- 在存储过程中调用子过程时,若子过程仍用原始参数,参数嗅探照常发生——局部变量必须在**最内层作用域**声明
- 使用
SET ARITHABORT ON/OFF不一致会导致同一语句生成多个缓存计划(sys.dm_exec_cached_plans中可见不同set_options值) - 覆盖索引虽能缓解,但治标不治本:一旦查询条件增加新字段,旧索引即失效,问题重现
真正难处理的,是那些参数组合多、数据分布无规律、又要求亚秒级响应的查询——这时候得结合统计信息更新频率、查询模板拆分、甚至应用层路由,而不是只盯着一个 hint。











