触发器不保证跨表事务原子性,其操作依赖宿主事务;mysql禁止commit/rollback,postgresql需显式return new/old,sql server中after触发器属父事务;慎用触发器,优先应用层事务。

触发器本身不保证跨表事务原子性
SQL 触发器运行在触发它的语句的事务上下文中,INSERT、UPDATE 或 DELETE 成功,触发器里的操作才一起提交;语句失败,触发器操作自动回滚。但前提是——所有操作都在同一个数据库连接、同一个显式或隐式事务里。一旦触发器里调用 COMMIT 或 ROLLBACK(比如 PostgreSQL 的 AUTONOMOUS TRANSACTION 模拟、MySQL 的存储过程误写),就会破坏原子性,甚至报错。
- MySQL 不支持自治事务,触发器内禁止
COMMIT/ROLLBACK,否则直接报错ERROR 1422 (HY000): Explicit or implicit commit is not allowed in stored function or trigger - PostgreSQL 默认也不允许,除非用
dblink或外部调用绕过,但这已脱离事务边界 - SQL Server 的
AFTER触发器天然属于父事务;但若在里面用OPENQUERY调远程服务器,就可能丢失一致性
业务逻辑放触发器前先问:它真需要“自动”执行吗?
把校验、计数、日志、状态更新塞进触发器,看似省事,实则容易埋雷。触发器不可见、难调试、测试成本高,且和 ORM、批量导入、ETL 工具等常有冲突。比如用 LOAD DATA INFILE 导入时,MySQL 默认跳过触发器(除非显式启用);Django 的 bulk_create 也不会触发 save() 相关逻辑,更不会激活数据库层触发器。
- 适合放触发器的场景:
updated_at自动更新、父子表级联计数(如订单行数同步到订单头)、审计字段(created_by基于 session 变量) - 不适合的场景:调用外部 API、发邮件、复杂条件分支、涉及多步写多个表且需重试逻辑
- 替代方案优先级:应用层事务封装 > 存储过程显式调用 > 触发器
MySQL 中触发器修改 NEW/OLD 的副作用
在 BEFORE INSERT 或 BEFORE UPDATE 触发器里改 NEW.column_name,确实会影响最终插入/更新的值,看起来像“拦截并修正”。但要注意:这种修改只对当前触发器生效,如果同一张表有多个 BEFORE 触发器,执行顺序由创建时间决定(MySQL 8.0+ 可用 FOLLOWS/PRECEDES 控制),极易导致逻辑覆盖或冲突。
-
NEW是可写的,OLD在INSERT中不存在,在DELETE中只读 - 不要在触发器里给
NEW.id赋值主键(除非是自增列且你明确想覆盖),否则可能违反唯一约束 - 避免在触发器中做耗时计算(如加密、正则匹配),会拖慢主 DML 语句,且无法异步
DELIMITER $$
CREATE TRIGGER orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.status IS NULL THEN
SET NEW.status = 'pending';
END IF;
SET NEW.created_at = NOW();
END$$
DELIMITER ;
PostgreSQL 中触发器函数返回值决定是否执行原操作
PostgreSQL 的触发器函数必须返回 TRIGGER 类型,且返回值直接影响原始语句行为:NULL 表示跳过原操作(相当于“拦截”),非 NULL(通常是 NEW 或 OLD)表示继续。这点和 MySQL 完全不同,稍不注意就会让 INSERT 静默失败。
- 在
BEFORE INSERT中返回NULL→ 插入被取消,无错误提示 - 在
BEFORE UPDATE中返回NEW→ 更新照常进行;返回OLD→ 字段不变,但语句仍算成功 - 务必在函数末尾写
RETURN NEW;(或OLD),漏写等于返回NULL










