存储过程性能退化主因是数据增长、执行计划退化和中间表滥用;当sys.dm_exec_query_stats中total_logical_writes突增,多因中间表泛滥或参数嗅探致计划失效。

不是存储过程本身“变老”,而是数据增长、执行计划退化、中间表滥用这三件事在 silently 恶化——尤其当 sys.dm_exec_query_stats 里 total_logical_writes 突增时,基本可以锁定是中间表泛滥或参数嗅探导致的计划失效。
执行计划被“骗”了:参数嗅探让同一存储过程走不同路径
SQL Server 第一次执行时用某个参数值生成执行计划,后续调用全复用这个计划。如果首次传的是“用户ID=1000000”(查不到数据),优化器可能选 Nested Loop;但下次传“用户ID=1”(返回5万行),仍硬套原计划,结果慢10倍。
- 现象:
EXEC sp_YourProc @uid = 1耗时2秒,@uid = 1000000耗时8毫秒,但反过来调用后,两个都变慢 - 验证方式:查
sys.dm_exec_cached_plans+sys.dm_exec_sql_text,看同一object_id是否对应多个 plan_handle - 临时解法:加
WITH RECOMPILE(只对高频低并发过程适用)或显式加OPTION (RECOMPILE)到关键语句 - 长期解法:用
OPTIMIZE FOR (@uid UNKNOWN)或拆分逻辑——比如小范围ID走索引查找,大范围走扫描+临时表预过滤
中间表反复创建:#temp 每次都在硬盘上重来一遍
#temp 表不是内存结构,每次执行都触发物理创建→插入→统计信息生成→删除。循环调用100次?就是100次 I/O + 锁 + 编译开销。
- 典型反模式:
SELECT * INTO #tmp FROM big_table WHERE ...放在 WHILE 循环里 - 检查信号:执行计划中连续出现多个
Table Insert+Clustered Index Scan - 能换就换:
@table_var适合 ≤100 行中间集,但别在它后面跟JOIN orders ON ...(优化器会估成1行,强制 Nested Loop) - 更优解:把
SELECT * FROM #tmp JOIN ...改成 CTE,前提是 CTE 只被引用一次;否则真要复用,得靠物化视图(SQL Server 2022+)或带索引的临时表(CREATE TABLE #tmp (...) WITH (DATA_COMPRESSION = ROW))
WHERE 条件没下推:先灌满再过滤,IO 白花了
写成 SELECT * INTO #t FROM orders 再 WHERE status = 1,等于把整张表搬进 tempdb;而 SELECT * INTO #t FROM orders WHERE status = 1 可能直接走索引查找,逻辑读从百万级降到几百。
- 关键动作:把过滤条件尽可能提前到最外层 FROM 子句里,而不是塞进中间表之后再筛
- 窗口函数替代分步:原逻辑「先建 #tmp1 算用户最近订单,再建 #tmp2 算等级,最后 JOIN」→ 压成一条:
FIRST_VALUE(order_date) OVER (PARTITION BY user_id ORDER BY order_date DESC) - 别信“先取全量再加工”:哪怕只是加个
ROW_NUMBER(),也比先SELECT * INTO #t再UPDATE #t SET rn = ...快得多
真正卡住人的,往往不是某条语句多慢,而是执行计划在数据分布变化后悄悄失效,加上中间表像雪球一样越滚越大——查 total_logical_writes 和执行计划里的 Actual Rows vs Estimated Rows 差异,比盯着代码更容易找到病灶。

















