必须将pg_trigger_depth() > 1判断置于触发器函数开头,结合TG_OP与TG_TABLE_NAME校验,以精准拦截同一表同事件类型的递归调用;BEFORE触发器中修改NEW字段则不会触发递归,是安全的零风险方案。

pg_trigger_depth() > 1 必须写在触发器开头
PostgreSQL 不拦截自更新,但每次 UPDATE 同表都会再触发一次,形成无限链式调用。pg_trigger_depth() 是唯一可靠的运行时判断依据,返回当前嵌套层级(首次为 1,递归第二次为 2)。它必须放在触发器函数第一行,否则后续 DML 已执行,防护失效。
常见错误是把它塞在 IF 条件里、或放在日志写入之后——等看到日志时,递归可能已跑满 100 层。正确写法:
IF pg_trigger_depth() > 1 THEN RETURN; END IF;
注意:pg_trigger_depth() 返回整数,不能和字符串比较;也不建议只判 > 2,因为第一次递归就该截断,留一层余量没意义。
TG_OP 和 TG_TABLE_NAME 要组合校验
单靠 pg_trigger_depth() > 1 可能误杀合法跨表触发。比如你有一个订单表触发器,在 AFTER INSERT 里 UPDATE 库存表,而库存表也有自己的触发器——这属于正常业务链,不该被拦。
真正要防的是“同一张表 + 同一事件类型”的重入。所以得加上上下文判断:
IF pg_trigger_depth() > 1 AND TG_OP = 'UPDATE' AND TG_TABLE_NAME = 'orders' THEN RETURN;- 避免硬编码表名,用
TG_TABLE_NAME动态获取,方便复用到其他表 - 如果触发器定义为
FOR EACH STATEMENT,NEW/OLD不可用,但TG_OP和TG_TABLE_NAME仍有效
BEFORE 触发器里改 NEW 行不触发递归
很多死循环其实没必要发生——比如自动填充 updated_at 或标准化字段,完全可以在 BEFORE UPDATE 中直接赋值给 NEW.updated_at := NOW(),不会引发二次触发。
这是 PostgreSQL 唯一允许的“本表修改”场景,且零风险:
-
NEW是即将写入的副本,修改它只是影响本次插入/更新结果 - 不要在
AFTER里做任何INSERT/UPDATE/DELETE当前表的操作,哪怕只改一行也会递归 - 如果逻辑必须依赖刚写入的完整行(比如生成审计记录),改用
PERFORM pg_notify('audit_event', row_to_json(NEW)::text)推出外部处理
连接级变量 SET LOCAL 不可靠
有人试过用 SET LOCAL trigger_skip = true 然后在触发器里查 current_setting('trigger_skip', true),这在多数情况下会失效。
原因很实在:SET LOCAL 只对当前事务生效,而触发器可能在子事务、SAVEPOINT 内运行,或者被 PL/pgSQL 的异常处理块重置。更糟的是,多个并发触发器共享同一个会话,current_setting 可能被覆盖或读错。
真正隔离、轻量、无副作用的方案只有两个:
- 用
pg_trigger_depth()—— 它天然按触发栈隔离,每个触发器调用都有独立深度值 - 用
tg_op+tg_table_name组合判断 —— 这些是触发器上下文变量,不依赖会话状态
别碰临时表、序列号、或新增 skip 字段——这些要么引入锁竞争,要么污染表结构,还增加维护成本。

















