trigger_nestlevel()不加参数会失效,因其返回当前会话所有触发器总嵌套层数,无法区分来源;正确做法是传入当前触发器object_id,如trigger_nestlevel(object_id(n'dbo.trg_orders_update'))。

直接用 TRIGGER_NESTLEVEL() 判断并 RETURN 是最简单有效的中断方式,但必须传入触发器对象 ID,不能只写 TRIGGER_NESTLEVEL() > 1 —— 否则在跨表触发链中会误判。
为什么 TRIGGER_NESTLEVEL() 不加参数会失效
SQL Server 的 TRIGGER_NESTLEVEL() 默认返回当前会话中所有触发器的总嵌套层数,不区分来源。比如 A 表触发器更新 B 表,B 表又有自己的触发器,这时 TRIGGER_NESTLEVEL() 返回 2,但你只想防 A 表自己递归,这个值就毫无意义。
- 正确做法是传入当前触发器的
OBJECT_ID:TRIGGER_NESTLEVEL(OBJECT_ID(N'dbo.trg_orders_update')) - 必须用
N'dbo.xxx'格式,字符串直接传会报错或始终返回 0 - 该函数在触发器执行初期就可用,放在第一行即可拦截
- 注意:它只对 AFTER 和 INSTEAD OF 触发器有效,DDL 触发器不支持
CONTEXT_INFO 比嵌套层数更可靠
当触发器可能被视图、OPENQUERY 或跨库调用间接激活时,TRIGGER_NESTLEVEL() 会不稳定。CONTEXT_INFO 是会话级二进制标记,完全由你控制,不受 SQL Server 内部计数逻辑干扰。
- 在业务代码发起关键更新前,先设标记:
SET CONTEXT_INFO 0x54524947474552(即 "TRIGGER" 的 ASCII 十六进制) - 触发器开头检查:
IF CONTEXT_INFO() = 0x54524947474552 RETURN - 标记长度固定为 128 字节,建议用纯 ASCII 编码,避免 Unicode 截断或空字节导致匹配失败
- 无需建表、不依赖锁、并发安全——每个会话独享一份
CONTEXT_INFO
AFTER 触发器里更新同表是最大雷区
只要你在 AFTER UPDATE 触发器中执行了对同一张表的 UPDATE,且该表还存在其他 AFTER UPDATE 触发器(哪怕不是同一个),SQL Server 就会压栈。它不判断是不是“同一个触发器”,只看“有没有另一个触发器能被这次 DML 激活”。
- 典型错误写法:
UPDATE orders SET updated_at = GETDATE() WHERE id IN (SELECT id FROM inserted) - 即使加了
IF UPDATE(status),只要最终语句改了原表,就仍会再进一次触发器 - 这种自引用递归,
RECURSIVE_TRIGGERS数据库开关完全无效,因为它是专为“用户定义的跨触发器链”设计的 - 真正起作用的是实例级
nested triggers开关,但它影响所有数据库,不能按需关闭
别信 @@NESTLEVEL 和 DISABLE TRIGGER
这两个方案在生产环境都踩过坑:
-
@@NESTLEVEL是存储过程/函数的嵌套深度,对触发器不准确;在某些执行路径(如通过视图触发)下返回值异常 -
DISABLE TRIGGER+ENABLE TRIGGER是 DDL 操作,有锁开销;并发请求可能互相覆盖,一个请求禁用后还没启用,另一个就进来了 - 临时表、全局变量、额外字段做标记,要么跨会话不可见,要么要改表结构,要么性能差
-
CONTEXT_INFO是唯一轻量、会话隔离、无需 DDL、无锁开销的可行方案
真正难的不是怎么中断,而是怎么让中断逻辑不破坏正常业务链——比如 A 表触发器更新 B 表,B 表触发器又要回写 A 表状态,这种双向依赖靠单点守卫拦不住,得从数据流设计上解耦。










