先验证是否为参数嗅探:查执行计划中ParameterCompiledValue与ParameterRuntimeValue是否错位,且执行计划出现索引扫描代替查找;再查query_stats中last_elapsed_time显著高于均值。

怎么确认真是参数嗅探在作怪
别一慢就加 OPTION (RECOMPILE),先验证是不是它。关键看编译值和运行值是否错位:查 sys.dm_exec_query_plan 提取 XML 执行计划,搜索 <ParameterList> 节点,对比 ParameterCompiledValue 和 ParameterRuntimeValue。如果前者是 '2020-01-01'、后者是 '2025-09-28',且执行计划里该走索引查找却用了扫描,基本坐实。
顺手跑下这个语句,看历史波动:
SELECT t.text, qs.avg_elapsed_time, qs.last_elapsed_time FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t WHERE t.text LIKE '%your_proc_name%' AND qs.last_elapsed_time > qs.avg_elapsed_time * 3;
结果里 last_elapsed_time 显著高于均值,就是典型参数嗅探信号。
用局部变量隔离是最轻量的解法
对大多数中等分布的参数(比如客户ID、状态码),直接声明同类型局部变量赋值,就能让优化器“看不见”真实参数值,转而依赖统计信息估算——既避免重编译开销,又比通用计划更贴近实际。
写法很简单:
CREATE PROC dbo.GetOrders @cust_id INT AS BEGIN DECLARE @cust_id_local INT = @cust_id; -- 关键这行 SELECT order_id, amount FROM Orders WHERE customer_id = @cust_id_local; -- 后续全用这个变量 END
注意三点:
- 变量必须在存储过程主体内声明,不能在
IF分支里临时声明再用 - 不能用于
IN子查询或EXEC(@sql)动态拼接场景 - 如果参数本身是
NULL或空字符串这类统计直方图里单独成桶的值,效果会打折扣
什么时候该上 OPTION (RECOMPILE)
OPTION (RECOMPILE) 不是加速器,是“急救包”——只在参数取值跨度极大、且 WHERE 条件完全由它主导时才值得用。比如时间范围从 1 天到 5 年、客户数据量从 10 行到 500 万行。
但必须警惕副作用:
- 高频调用(如每秒 20+ 次)会导致 CPU 持续升高,
sys.dm_exec_query_stats里plan_generation_num会频繁跳变 - 无法响应后续统计信息更新,哪怕你刚跑了
UPDATE STATISTICS,它下次还是重新编译,不复用新统计 - 别给整个存储过程加
WITH RECOMPILE,粒度太粗;优先只加在卡顿最狠的那条SELECT或UPDATE末尾
示例:
SELECT * FROM Sales WHERE order_date >= @start_date AND order_date < @end_date OPTION (RECOMPILE);
OPTIMIZE FOR UNKNOWN 更适合稳定 OLTP 场景
如果你的存储过程每分钟被调用几十次,且参数虽有变化但没极端倾斜(比如订单状态在 'Pending'/'Shipped'/'Cancelled' 间均匀分布),OPTION (OPTIMIZE FOR UNKNOWN) 是比 RECOMPILE 更稳的选择。
它让优化器彻底忽略参数值,纯靠列统计信息的平均密度估算行数,生成一个“中庸但可靠”的计划。缺点是:遇到真实数据分布严重偏斜(比如 99% 是 'Active',1% 是 'Archived'),可能不如 RECOMPILE 精准。
验证方式很直接:开启 SET STATISTICS XML ON,看执行计划里 Estimated Number of Rows 是否落在业务常见量级附近。如果加了提示后估算值从 1 跳到 50 万,而你日常查的客户通常有 10–20 万订单,那就对了。
容易踩的坑是写成 OPTIMIZE FOR (@p = NULL)——NULL 在直方图里是独立桶,常导致全表扫描;真要覆盖空值场景,得配合 OR @p IS NULL 逻辑,或改用局部变量方案。

















