根本原因是ssms与应用程序(如c#)因arithabort等set选项不同,触发sql server缓存了多个执行计划,导致各自复用效率迥异的计划;ssms默认arithabort on,而.net应用默认off,使同一存储过程生成并使用两套独立计划。

根本原因不是代码写错了,而是 SQL Server 为同一存储过程缓存了多个执行计划,而 SSMS 和你的应用程序(比如 C#、Java)触发的是不同计划——通常一个高效、一个低效。
为什么 SSMS 和程序用的不是同一个执行计划
SQL Server 缓存执行计划时,会把连接会话的 SET 选项(尤其是 ARITHABORT)作为缓存键的一部分。SSMS 默认是 ARITHABORT ON,而多数 .NET 应用(如 SqlConnection)默认是 ARITHABORT OFF。哪怕只是这个开关不同,SQL Server 就认为是“完全不同的查询”,会生成并缓存两套独立计划。
- 你第一次在 SSMS 里执行,生成了 Plan A(
ARITHABORT ON),它恰好高效 - 程序首次调用时,因
ARITHABORT OFF触发 Plan B,而 Plan B 是基于某次异常参数值生成的,走了全表扫描或嵌套循环等低效路径 - 后续所有同设置的调用都复用 Plan B,所以程序一直慢
怎么快速验证是不是执行计划问题
别猜,直接查缓存。在 SSMS 中运行:
SELECT
cp.objtype,
cp.usecounts,
cp.size_in_bytes,
st.text,
qp.query_plan
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp
WHERE st.text LIKE '%你的存储过程名%'
看结果里是否出现多条记录,且 usecounts 差异大、size_in_bytes 接近但 query_plan 完全不同——这就是双计划共存的铁证。
- 如果只有一条,问题大概率不在缓存,需查阻塞、网络或应用层超时配置
- 如果有两条以上,且计划 XML 中的
<parameterlist></parameterlist>显示参数值差异极大(比如一个传 1,一个传 999999),基本锁定参数嗅探
临时修复:强制刷新计划缓存
最简单有效的临时手段是让 SQL Server 抛弃旧计划,下次调用时重新编译:
EXEC sp_recompile '你的存储过程名';
注意:sp_recompile 不修改存储过程逻辑,只标记为“需重编译”。下次任意客户端调用时,SQL Server 会根据当前会话的 ARITHABORT 设置和实际参数值生成新计划。
- 执行后立刻测试程序调用,90% 的情况会恢复正常
- 如果仍慢,说明新生成的计划又踩中了参数嗅探陷阱(比如程序首次调用用了边缘值)
- 不要在生产高峰期频繁执行,避免大量重编译带来 CPU 尖峰
长期规避:从代码和部署层面控制计划生成
靠手动 sp_recompile 治标不治本。真正稳定的做法是统一环境或绕过缓存歧义:
- 在 C# 中显式开启
ARITHABORT:连接字符串加Connection Timeout=30;ARITHABORT=true;,或执行SET ARITHABORT ON作为第一条命令 - 对关键存储过程加
WITH RECOMPILE(慎用):每次调用都重编译,适合参数分布极不均匀、且执行频率不高的场景 - 改用
OPTION (OPTIMIZE FOR (@param = 常见值)):在存储过程中明确指定优化目标,避免 SQL Server 自主“嗅探”出错 - 避免在存储过程中把参数赋给本地变量再查询:SQL Server 对本地变量不做参数嗅探,一律按 30% 估算行数,极易走错计划
最隐蔽也最容易被忽略的点:不是所有“慢”都来自执行计划。如果 sp_recompile 后依然慢,立刻检查应用程序是否设置了过短的 CommandTimeout(比如默认 30 秒),而实际执行耗时刚好卡在临界点——这时看到的“超时”其实是假象,真实瓶颈可能在锁等待或 I/O 上。











