mysql触发器禁止直接更新自身表,会立即报error 1442;跨库操作仍可能递归;安全做法是写入中间表或用会话变量标记,sql server推荐context_info,postgresql需结合pg_trigger_depth()与tg_op精细控制。

MySQL触发器里UPDATE自己表直接报错,不是死循环而是语法拒绝
MySQL根本不会让你走到“循环”那步——只要触发器体里出现对本表的UPDATE、INSERT或DELETE,立刻抛出ERROR 1442。哪怕加了IF OLD.status != NEW.status判断也无效。这不是配置问题,是内核级硬限制。
真正可能递归的场景是:AFTER INSERT触发器去UPDATE other_db.users,而目标库那张表也有触发器,且它又反向更新原表。跨库不等于安全。
- BEFORE触发器中只操作
NEW.col是安全的,不触发新事件 - AFTER触发器若真要落库(如记日志),必须写到中间表:
INSERT INTO trigger_queue (table_name, pk, action) - 会话变量
@skip_my_trigger可作跳过标记,但需手动在业务SQL前设值、执行后清空,否则同连接后续语句也被跳过
SQL Server用CONTEXT_INFO做会话级标记最可靠
@@NESTLEVEL不可靠——它不区分“合法嵌套”(比如视图触发)和“自调用”,数值可能误判。DISABLE TRIGGER更危险:并发请求下,一个禁用还没启用,另一个已闯入;DDL本身还带锁开销。
CONTEXT_INFO是128字节二进制会话变量,每个连接独享,不会被嵌套覆盖,且无需建表、不改结构。
- 业务SQL执行前设标记:
SET CONTEXT_INFO 0x54524947474552("TRIGGER"的ASCII十六进制) - 触发器开头立即检查:
IF CONTEXT_INFO() = 0x54524947474552 RETURN - 避免用
CONVERT(VARCHAR, CONTEXT_INFO())比较——编码或截断会导致失效 - 若触发器被存储过程调用,过程开头设标记、结尾清空:
SET CONTEXT_INFO 0x0
PostgreSQL靠pg_trigger_depth() + TG_OP组合拦截
pg_trigger_depth()返回当前触发器嵌套层数(首次为1),但它只反映层级,不校验事件类型。单看数值容易漏掉间接触发——比如触发器调用了函数,函数里又更新了本表。
必须结合TG_OP做精细控制,否则防护形同虚设。
- 基础防护:
IF pg_trigger_depth() > 1 THEN RETURN; END IF; - 推荐写法:
IF pg_trigger_depth() > 1 AND TG_OP = 'UPDATE' THEN RETURN; - 该函数只对当前触发器生效,无法拦截由函数间接引发的再触发
- 所有运行时标记方案都依赖执行上下文的一致性——SQL Server的
CONTEXT_INFO、PostgreSQL的pg_trigger_depth(),一旦上下文被复用(如连接池未重置),标记就失效
别信数据库开关,RECURSIVE_TRIGGERS对自引用完全无效
SQL Server的RECURSIVE_TRIGGERS选项默认OFF,但它只控制“A触发B、B再触发A”这类用户定义嵌套,对“A触发A”自引用毫无约束力。MySQL压根不提供这个开关,直接语法拦截。PostgreSQL默认放行递归,全靠你手动拦。
试图靠ALTER DATABASE SET RECURSIVE_TRIGGERS OFF来防死循环,等于给门装锁却忘了窗开着。
- MySQL:
SET SESSION max_sp_recursion_depth = 1是唯一能阻断链式递归的会话级设置,设为0等于放弃防护 - 连接池场景(如pymysql)必须每次获取连接后重置该变量,否则可能复用到之前设为0或2的连接
- 所有标记方案(
CONTEXT_INFO、@var、pg_trigger_depth())的前提是:执行上下文干净且可控——这点最容易被忽略










