存储过程快的前提是执行计划被成功缓存并复用;若因WITH RECOMPILE、EXEC(@sql)、OPTION(RECOMPILE)或参数类型不一致导致缓存失效,则可能比参数化即席查询更慢。

先说一个核心事实:存储过程并不“天然”比直接写 SQL 快。它的速度优势,完全建立在执行计划被成功缓存并复用的基础上。一旦缓存失效、参数化不当或者语句结构破坏了可重用性,存储过程的表现可能还不如参数化的即席查询。
执行计划是否真的被复用了?查 sys.dm_exec_cached_plans 和 sys.dm_exec_query_stats
缓存这事儿,可不是个黑箱。SQL Server 提供了清晰的视图来验证。光看“执行时间变短”可不够,必须确认是否真的命中了同一个 plan_handle。具体怎么查?分三步走:
- 执行一次目标存储过程后,运行以下查询:
SELECT plan_handle, cacheobjtype, objtype, usecounts, size_in_bytes FROM sys.dm_exec_cached_plans WHERE objtype = 'Proc'
找到对应存储过程的plan_handle。 - 接着,用这个
plan_handle去查sys.dm_exec_query_stats,看看execution_count是否随着每次调用而递增。 - 如果发现
usecounts == 1且execution_count纹丝不动,那就说明每次都在编译新计划。这通常意味着存储过程内部使用了WITH RECOMPILE、OPTION (RECOMPILE),或者存在EXEC(@sql)这类动态拼接。
哪些写法会让执行计划缓存彻底失效?
缓存失效往往不是偶然,而是由一些明确的语法或选项触发的。下面这些就是常见的“踩坑点”:
- 在存储过程中使用
EXEC(@sql)或sp_executesql N''来动态拼接完整语句(哪怕只是拼接 WHERE 条件),都会导致无法生成稳定的执行计划。 - 显式加上
WITH RECOMPILE选项,这会强制每次执行都重新编译。虽然适用于数据分布剧烈变化的特殊场景,但也彻底抹杀了缓存带来的性能收益。 - 在语句末尾添加
OPTION (RECOMPILE),它的作用粒度更细,但同样会绕过缓存机制。 - 参数类型不一致:比如客户端传递的是
int类型,而存储过程参数定义为smallint,可能触发隐式转换并导致计划重编译。 - 当数据库兼容级别低于 120 时,简单参数化的规则会更加严格,部分即席语句也难以进入缓存。这种情况下,存储过程的优势反而会更明显一些。
网络往返和语句打包带来的实际收益,常被低估
执行计划缓存只是故事的一面。对于高频、多步骤的操作,减少网络往返(RPC)次数才是更实在的提速关键。
- 设想一个逻辑:插入主表 + 插入明细 + 获取
SCOPE_IDENTITY()+ 记录日志。如果封装成一个存储过程,只需 1 次网络请求。但如果拆分成 4 条独立的 SQL 语句发送,至少会产生 4 倍的 TCP 往返延迟。 - 如果客户端采用字符串拼接的方式发送
INSERT INTO t VALUES ('a', 'b', ...),由于每行的值都不同,每次都会被视作全新的语句,从而走一遍完整的编译流程,缓存利用率几乎为零。 - 存储过程的参数传递的是二进制值,而非文本 SQL。一个 100 字符的参数名加上一个 4 字节的整数值,其数据量远小于发送一条 200 字符的完整 INSERT 语句。
所以,真正决定性能高低的,并不是“用没用存储过程”这个标签,而是“执行计划有没有稳稳地留在 sys.dm_exec_cached_plans 里,以及你有没有无意中用 EXEC 或 RECOMPILE 这类操作把它踢出去”。理解并验证这一点,才是性能优化的关键所在。
