存储过程慢90%源于sql未被正确引导执行,需用explain或set statistics xml on查执行计划,重点观察rows read、estimated rows和warnings;索引失效常因where中对索引列使用函数;参数嗅探会导致执行计划不适配不同参数;重复扫描大表应改用带覆盖索引的临时表;子查询优先转join或提为变量;not in需替换为not exists;调优关键在于基于真实数据分布做缓存、重编译或拆分决策。

直接结论:存储过程里慢,90%不是存储过程本身的问题,而是里面那几条SQL没被正确引导执行。
查执行计划必须用 EXPLAIN 或 SET STATISTICS XML ON
不看执行计划就调优,等于蒙眼修车。SQL Server 用 SET STATISTICS XML ON,MySQL/PostgreSQL 用 EXPLAIN ANALYZE。重点盯三处:Rows Read(实际扫描行数)、Estimated Rows(预估是否严重偏离)、Warnings(比如“Missing index”或“Convert data type”)。如果某步显示 Clustered Index Scan 而不是 Seek,说明索引没被用上——别急着加索引,先确认 WHERE 条件是否对索引列做了函数操作,比如 WHERE YEAR(CreateTime) = 2025 就会让索引失效。
参数嗅探(Parameter Sniffing)是隐形杀手
同一个存储过程,传 @BeginDate = '2025-01-01' 很快,传 @BeginDate = '2025-07-01' 却卡住,大概率是参数嗅探:SQL Server 拿第一次的参数值生成了执行计划,缓存后复用,但后续参数导致数据分布差异大,原计划不再适用。
- 临时验证:在存储过程中加
OPTION (OPTIMIZE FOR (@BeginDate = '2025-01-01'))看是否恢复快 - 长期方案:对关键查询加
OPTION (RECOMPILE)(适合参数变化大、执行频率不高) - 折中方案:用局部变量绕过嗅探,如
DECLARE @LocalDate DATE = @BeginDate; WHERE CreateTime > @LocalDate
别让存储过程反复读同一张大表
如果逻辑里要对 A、B、C 三表 JOIN 后的结果做 4 次不同分组统计,每次都在原始表上跑一遍,I/O 和 CPU 就翻了 4 倍。这时该用临时表:
- 用
SELECT ... INTO #Temp FROM A JOIN B JOIN C WHERE ...一次性提取核心数据 - 立刻在
#Temp上建覆盖索引,比如CREATE NONCLUSTERED INDEX IX_Temp_Key ON #Temp (a3, a4, c1) - 后续所有
GROUP BY、SUM、WHERE都基于#Temp,避免重复扫描原始大表
注意:临时表只在当前会话有效,不用显式 DROP;但如果存储过程可能并发执行,别用全局临时表 ##Temp,容易冲突。
子查询优先转 JOIN,尤其在 WHERE 里嵌套
像这种写法:WHERE id IN (SELECT id FROM Orders WHERE status = 1),在 SQL Server 中很可能触发 Nested Loop + Table Scan,性能随子查询结果集增大急剧恶化。
- 改成
INNER JOIN Orders o ON t.id = o.id AND o.status = 1,让优化器有机会选 Hash Join 或 Merge Join - 如果子查询只返回单值(比如
(SELECT MAX(price) FROM items)),可提成变量DECLARE @MaxPrice DECIMAL = (SELECT MAX(price) FROM items),避免每次循环都执行 - 慎用
NOT IN,空值会让整个条件判为UNKNOWN,结果为空;改用NOT EXISTS更可靠
真正难的不是加索引或改语法,而是判断哪一步该缓存、哪一步该重编译、哪一步该拆出去——这些决策依赖于数据分布的真实波动,而不是理论上的“应该”。上线前,拿生产环境最近一周的典型参数跑几轮,比任何静态分析都管用。










