临时表建索引或alter会触发重编译,因sql server将此类ddl视为架构变更;统计信息超阈值(rt=500+0.2n)或跨作用域访问失败也会引发重编译;应一次性定义结构、用select into替代运行时建索引、优先使用表值参数或表变量,并统一set选项。

临时表建索引或ALTER触发重编译
SQL Server 把 #temp 表上的 CREATE INDEX、ALTER TABLE #t ADD 等 DDL 操作视为“架构变更”,哪怕只在过程内部执行,也会立刻让当前执行计划失效。这不是 bug,是设计行为——优化器必须确保后续查询看到的是最新结构。
常见错误写法:CREATE TABLE #t (id INT); INSERT INTO #t ...; CREATE CLUSTERED INDEX IX ON #t (id); 这段代码必然触发一次重编译。
- 正确做法:所有
#temp表结构(含主键、非聚集索引)必须在过程开头一次性定义完整,后续绝不ALTER - 若需加速查询,优先用
SELECT col INTO #t FROM ... ORDER BY col隐式生成带聚簇的临时表 - 避免反复创建/删除多个临时表(如
#t1、#t2),改用单个宽表 + 标志列区分逻辑
临时表统计信息自动更新引发重编译
临时表插入或删除行数超过统计阈值(RT),SQL Server 会自动更新其统计信息,进而触发 StatisticsChanged 类型重编译。这个阈值比普通表更低:IF n > 500 THEN RT = 500 + 0.20 * n,其中 n 是上次统计采集时的行数。
现象:调用同一存储过程多次后,sys.dm_exec_query_stats 中 plan_generation_num 明显大于 execution_count,且 last_elapsed_time 波动剧烈。
- 验证方法:在过程开头加
DBCC SHOW_STATISTICS('#t', '_WA_Sys_...'),查Rows Modified是否接近或超过 RT - 缓解方案:对高频写入的临时表,可在建表后立即加
OPTION (KEEPFIXED PLAN)(仅禁用统计驱动重编译,不防 DDL 触发) - 更稳妥替代:大数据量场景优先用表变量
@t TABLE(...)(无统计信息,不触发此类型重编译),但注意它默认按 1 行估算
临时表跨作用域访问失败导致隐式重编译链
本地临时表 #t 严格限制在定义它的存储过程中。子过程无法访问,但很多人会写 IF OBJECT_ID('tempdb..#t') IS NOT NULL SELECT * FROM #t —— 实际永远为 NULL,结果子过程只能重新建表、再 INSERT,等于重复执行 DDL + DML,每次调用都新增一个重编译点。
- 绝对不要在子过程中检查或引用父过程的
#temp表 - 传中间结果用表值参数
@tvp(需提前定义 TYPE),支持统计信息、可建索引、不触发重编译 - 若必须共享状态,可用只读全局临时表
##t_readonly,但必须显式DROP TABLE ##t_readonly,且注意并发冲突风险
SET 选项不一致让临时表计划彻底无法复用
哪怕两个存储过程逻辑完全一样,只要一个连接设了 SET ARITHABORT ON、另一个没设,SQL Server 就视为不同上下文,各自编译、各自缓存。而临时表的计划缓存对 SET 更敏感——因为它的元数据存在于 tempdb,跨会话隔离更强。
- 实操检查:
SELECT usecounts, cacheobjtype, objtype FROM sys.dm_exec_cached_plans WHERE usecounts = 1 AND text LIKE '%#t%',大量usecounts = 1很可能就是SET不一致所致 - 统一做法:在每个存储过程开头显式设置
SET ARITHABORT ON; SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON; - 排查客户端:某些 ORM(如旧版 Entity Framework)默认关闭
ARITHABORT,而 SSMS 默认开启,容易造成计划分裂
临时表本身不是问题,问题出在“动态性”上:DDL、统计更新、作用域越界、环境漂移——这些都会让 SQL Server 主动放弃缓存。真正要控制的不是“用不用临时表”,而是“怎么用才不让优化器觉得计划不可信”。











