直接禁用数据库级递归开关(如RECURSIVE_TRIGGERS OFF)不能解决“触发器更新自己表”引发的死循环——SQL Server对自引用触发器完全不认该开关,MySQL则直接报ERROR 1442;真正可控解法是在触发器内部加运行时标记:SQL Server用CONTEXT_INFO做会话级标记,MySQL设SET SESSION max_sp_recursion_depth = 1,PostgreSQL用pg_trigger_depth() > 1判断。

直接禁用数据库级递归开关(如 RECURSIVE_TRIGGERS OFF)不能解决“触发器更新自己表”引发的死循环——SQL Server 对自引用触发器完全不认这个开关,MySQL 则根本禁止这种操作并直接报 ERROR 1442。真正可控、可落地的解法,是在触发器逻辑内部加运行时标记。
SQL Server:用 CONTEXT_INFO 做会话级执行标记
这是目前最轻量、最可靠的方式,不依赖临时表、不改表结构、不污染业务字段,且每个会话隔离:
-
CONTEXT_INFO是 128 字节的二进制会话变量,可在触发器中读写,且不会被嵌套调用覆盖 - 在业务 SQL 执行前设标记:
SET CONTEXT_INFO 0x54524947474552(即 "TRIGGER" 的 ASCII 十六进制) - 触发器开头立即检查:
IF CONTEXT_INFO() = 0x54524947474552 RETURN - 避免用字符串比较(如
CONVERT(VARCHAR, CONTEXT_INFO())),易因截断或编码问题失效 - 若触发器被存储过程调用,需确保该过程也在开头设标记、结尾清空(
SET CONTEXT_INFO 0x0)
MySQL:必须设 max_sp_recursion_depth = 1 会话级限制
MySQL 不允许触发器内 UPDATE 自身表,但允许跨表触发;而递归深度失控才是死循环主因——它默认不限制(max_sp_recursion_depth = 0):
- 该变量只能在会话级设置:
SET SESSION max_sp_recursion_depth = 1,不能在触发器体内写SET - 连接池场景(如
pymysql)必须每次获取连接后重置,否则可能复用到之前设为 0 或 2 的连接 - 设为 1 表示“原始语句触发一次,内部再触发就拒绝”,这是唯一能真正阻断链式递归的值
- 设为 0(不限制)等于放弃防护;设为 2 仍可能在第三层崩,且无法预判哪一层是安全边界
- 注意:此设置对存储过程也生效,若触发器里调用了含 UPDATE 的 SP,同样会被拦
PostgreSQL:靠 pg_trigger_depth() + tg_op 组合判断
PostgreSQL 默认不禁止递归,但提供运行时函数精准识别当前触发层级和事件类型:
-
pg_trigger_depth()返回当前触发器嵌套层数,首次触发为 1,递归第二次为 2,以此类推 - 在触发器开头加:
IF pg_trigger_depth() > 1 THEN RETURN; END IF; - 更精细控制可结合
TG_OP(如只在AFTER UPDATE且深度 > 1 时跳过) - 慎用
current_setting('app.trigger_disabled')类会话变量:需提前SET LOCAL,且仅对当前事务有效 - 不要依赖
pg_backend_pid()拼临时表名做标记——并发下 PID 可能重复,且清理成本高
最容易被忽略的一点:所有标记方案都要求“业务入口统一”。哪怕只漏一个直连脚本、一个未封装的运维 SQL、一个旧版应用连接,就可能绕过标记导致循环重启。标记不是银弹,而是把控制权从数据库配置拉回到代码可审计的路径上。

















