触发器中使用临时表极易引发TempDB闩锁争用,因其高频建删操作集中争抢GAM/SGAM/PFS页;优化应优先改用表变量、确保tempdb多文件配置,并将非必要逻辑移出触发器。

触发器里用临时表,基本等于在高并发路口扔下一颗定时炸弹——TempDB 闩锁争用大概率立刻抬头。这不是危言耸听,而是 SQL Server 内部分配机制决定的:每个 #t 创建都得抢同一组 GAM/SGAM/PFS 页,单文件 tempdb 下尤其明显。优化方向不是“怎么写得更漂亮”,而是“能不能不写”或“能不能绕开分配热点”。
为什么触发器里的 #temp 特别伤 TempDB
触发器运行在事务上下文中,且多数是短生命周期 + 高频调用(比如订单插入、日志记录)。这意味着:
- 每次触发都新建
#t→ 触发 tempdb 数据页分配 → 争抢 GAM/SGAM 页 - 如果触发器嵌套或批处理中多次执行,
#t可能反复建删 → PFS 页面更新频繁 - 即使只查不改,
SELECT ... INTO #t仍需分配空间,且无法复用缓存计划中的工作表 - 版本存储被激活时(如启用了 RCSI),
#t的 DML 还会额外写入行版本 → 加重 version_store_kb 消耗
替代方案:优先用表变量代替 #temp
表变量(@t)不走 tempdb 分配路径,不生成事务日志,也不会触发 PFS/GAM 争用——只要数据量不大(一般 ≤ 100 行)、不涉及复杂连接或索引,就是更安全的选择。
-
@t的元数据在内存中维护,不修改系统分配页 - SQL Server 2019+ 对
@t的统计信息支持已增强,执行计划质量提升明显 - 注意:不要给
@t加非聚集索引(语法允许但无实际效果),也不要用SELECT * INTO @t(非法) - 反例:避免
INSERT @t SELECT ... FROM big_table WHERE ...带大结果集,此时仍可能溢出到 tempdb
真要保留临时表?至少做三件事
如果业务逻辑强依赖 #t(比如需要中间索引、多次 JOIN、或结果集 > 500 行),那就必须控制它的破坏力:
- 显式指定
CREATE TABLE #t (...)结构,禁用SELECT ... INTO #t—— 后者无法复用计划,且每次都会重新分配空间 - 在触发器开头加
IF OBJECT_ID('tempdb..#t') IS NOT NULL DROP TABLE #t,防止异常退出后残留(虽 tempdb 重启清空,但运行中残留会干扰监控) - 确保 tempdb 已按 CPU 核心数配置多文件(例如 16 核服务器设 8 个等大文件),否则再怎么改触发器也扛不住 PAGELATCH_UP 等待
更彻底的解法:把逻辑移出触发器
触发器本质是“隐式耦合”,临时表只是表象,根子在设计。真正压垮 TempDB 的,往往是那些本不该在事务中完成的聚合、日志归档、通知推送等操作。
- 用异步方式拆分:把临时计算结果写入持久表 + Service Broker 或轮询任务消费
- 改用计算列或索引视图替代触发器内联计算,避免每次 INSERT 都跑一遍逻辑
- 检查是否误用触发器实现业务规则——这些规则更适合放在应用层或存储过程中统一管控
最常被忽略的一点:哪怕你把触发器写得再精简,只要它还在事务里创建临时对象,就始终在和所有其他会话共享那几页关键系统页。真正的优化,永远始于“这个动作真的必须此刻发生吗”。










