SQL Server触发器自修改会导致无限递归,应使用CONTEXT_INFO会话级标记避免;SET RECURSIVE_TRIGGERS无效,@@NESTLEVEL和DISABLE TRIGGER方案不可靠。

触发器里改自己表导致无限递归
SQL Server 默认允许触发器修改触发它的同一张表,UPDATE 触发器里再执行 UPDATE,就会触发自身,形成死循环——不是报错停住,而是卡住连接、耗尽资源,甚至拖垮整个实例。
根本原因在于:SQL Server 的 RECURSIVE_TRIGGERS 数据库选项默认是 OFF,但这个开关只控制「用户定义的嵌套触发器」(比如 A 触发 B,B 又触发 A),对「自引用触发器」(A 触发 A)完全不生效。
- 别依赖
SET RECURSIVE_TRIGGERS ON想拦住自调用,它没用 - 不要在触发器里写
UPDATE target_table SET ... WHERE ...且条件可能命中刚被触发的行 - 哪怕加了
IF UPDATE(col)判断,只要最终语句改了原表,就仍会再进一次触发器
用上下文信息做执行标记最可靠
靠临时表、全局变量或额外字段存标记都不可靠:临时表跨会话不可见;@@SPID 在并行执行下可能重复;加字段要改表结构,影响业务。
SQL Server 提供了轻量、会话级、无需建表的方案:CONTEXT_INFO。它是一段 128 字节的二进制数据,每个会话独享,且在触发器内可读可写,完美适配“本次操作是否已由触发器发起”的判断场景。
- 在触发器开头读
CONTEXT_INFO(),如果值等于预设标记(如0x54524947474552表示 "TRIGGER"),直接RETURN - 在业务代码真正要触发更新前,先设标记:
SET CONTEXT_INFO 0x54524947474552 - 标记值建议用固定字节数的 ASCII 编码,避免 Unicode 或空字节引发截断问题
示例片段:
CREATE TRIGGER tr_prevent_loop ON orders
AFTER UPDATE
AS
BEGIN
IF CONTEXT_INFO() = 0x54524947474552
RETURN;
<pre class='brush:php;toolbar:false;'>-- 正常逻辑:比如同步更新 order_status_log
INSERT INTO order_status_log (order_id, status, updated_at)
SELECT i.order_id, i.status, GETDATE()
FROM inserted i;END
为什么不用 @@NESTLEVEL 或触发器禁用?
@@NESTLEVEL 看起来能数嵌套层数,但 SQL Server 对触发器的嵌套计数不区分「自调用」和「合法嵌套」,且在某些执行路径(如通过视图触发)下行为不稳定。
至于 DISABLE TRIGGER + ENABLE TRIGGER,风险极高:并发请求可能互相干扰,一个请求禁用后还没启用,另一个请求就进来了;而且 DDL 操作本身有锁开销,容易引发阻塞。
-
@@NESTLEVEL > 1不是安全边界,第一次自调用时它就是 2,但你无法知道这 2 是来自业务还是触发器 -
DISABLE TRIGGER必须用动态 SQL 才能在触发器里执行,而动态 SQL 会重置执行上下文,CONTEXT_INFO也会丢失 - 有些 ORM 或中间件会自动包装事务,导致
DISABLE/ENABLE跨事务失效
注意 CONTEXT_INFO 的生命周期和并发安全
CONTEXT_INFO 绑定到会话(session),不是事务,也不是触发器作用域。这意味着:它在存储过程、触发器、普通批处理中都能读写,但也意味着你必须确保「设标记」和「触发更新」在同一个会话里完成。
- Web 应用用连接池时,每次请求可能复用不同物理连接,不能在应用层设一次
CONTEXT_INFO就指望后续所有 DB 操作都继承 - 必须在业务 SQL 前紧挨着执行
SET CONTEXT_INFO,例如在同一个EXEC或存储过程中,不能分两次独立查询发送 - 不需要手动清空
CONTEXT_INFO,会话断开自动释放;但如果想提前复位,可用SET CONTEXT_INFO 0x
最易被忽略的一点:如果你的触发器逻辑里调用了其他存储过程,而那个过程内部又触发了同表更新,那它不会自动继承外层的 CONTEXT_INFO 设定——必须在那个过程里也检查、也设,或者把标记逻辑下沉封装成通用校验函数。

















