SQL Server存储过程缓存命中需看plan_cache_hit是否发生;通过SET STATISTICS XML ON执行后检查XML计划中CachePlan节点及CompileTime变化,或查sys.dm_exec_query_stats中usecounts≥2且plan_handle稳定复用。

SQL Server 存储过程缓存是否命中,怎么看?
SQL Server 对存储过程的执行计划默认会缓存,但「缓存存在」不等于「被复用」。真正关键的是 plan_cache_hit 是否发生——即后续调用是否直接复用了已编译的执行计划,而非重新编译。
查缓存命中率不能只看 sys.dm_exec_cached_plans 里有没有你的过程,得结合实际执行上下文。最直接的方式是开启查询计划和统计信息:
SET STATISTICS XML ON; EXEC YourStoredProcedure @param = 123;
执行后看返回的 XML 计划中是否有 CachePlan 节点,以及 CompileTime 是否显著高于后续执行(反复执行同一语句,首次高、后续低,说明命中了)。
- 若每次执行都出现新
plan_handle(查sys.dm_exec_query_stats),大概率没命中 -
usecounts字段值 ≥ 2 才算稳定复用,=1 表示仅被用过一次或刚加载进来 - 注意:
DBCC FREEPROCCACHE会清空全部,测试时慎用;建议用DBCC FLUSHPROCINDB清指定库更安全
参数嗅探导致缓存计划失效的典型表现
当存储过程中使用了参数(尤其是 WHERE 条件含 @id、@status),SQL Server 会基于**首次传入值**生成并缓存执行计划。如果后续传入值选择性差异极大(比如第一次查用户ID=1,第二次查 status='inactive' 占95%行),旧计划可能严重低效,甚至触发重编译(表现为 reason = 'Parameter Sniffing')。
查重编译原因可运行:
SELECT deqs.plan_handle, deqs.statement_text, deqs.last_execution_time,
deqs.execution_count, deqb.query_plan
FROM sys.dm_exec_query_stats deqs
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) dest
CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) deqb
WHERE dest.text LIKE '%YourStoredProcedure%';
- 重点关注
execution_count高但plan_handle频繁变化的情况 - 在过程内对关键参数加
OPTION (RECOMPILE)可强制每次生成新计划,适合值分布极不均匀的场景 - 用
WITH RECOMPILE创建过程(CREATE PROC ... WITH RECOMPILE)会让整个过程永不缓存,开销大,仅用于调试或极短生命周期过程
SET 选项不一致让缓存计划直接作废
SQL Server 要求两次执行的会话级 SET 选项完全一致,才会考虑复用缓存计划。常见冲突项包括:ANSI_NULLS、QUOTED_IDENTIFIER、ARITHABORT。
尤其注意:SSMS 默认 ARITHABORT = ON,而某些 ORM(如老版 Linq2Sql 或 ADO.NET 直连未显式设置)可能默认 OFF,导致同一个存储过程在 SSMS 和应用中生成两套缓存计划,互相不可见。
- 检查当前会话 SET 状态:运行
DBCC USEROPTIONS - 应用层连接字符串中加上
Packet Size=4096;ArithAbort=True;(.NET)可统一行为 - 建过程时务必用
SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON;开头,否则后续不同 SET 下可能无法匹配已有缓存
临时表与表变量对缓存的影响差异
在存储过程中创建 #temp 临时表,不会阻止主执行计划缓存;但若在临时表上建索引(CREATE INDEX IX ON #t(col)),SQL Server 会为该过程生成多个缓存条目(每个索引组合一个),增加查找开销和内存占用。
而 @table_variable 不参与统计信息收集,优化器始终按「估计 1 行」生成计划,容易选错连接方式(如嵌套循环 vs 哈希匹配),且其计划无法被其他会话复用(作用域隔离)。
- 数据量 > 100 行时,优先用
#temp+ 显式CREATE INDEX,并加OPTION (RECOMPILE)让优化器感知真实行数 - 避免在循环中反复
CREATE TABLE #t,改用TRUNCATE TABLE #t复用结构 - 表变量不适合做 JOIN 主表,尤其当它作为驱动表时,性能抖动明显
缓存不是越久越好,也不是越多越好。真正要盯住的,是「同一逻辑请求是否稳定获得相同高效计划」——这比单纯看 usecounts 或缓存大小重要得多。

















