mysql触发器内禁止使用start transaction、commit、rollback等事务控制语句,因其无独立事务上下文;before insert中new.id不可用,须用after insert获取;update触发器需用null安全比较(如old.col new.col);delete触发器中old只读且不可多次引用。

触发器里不能用事务控制语句
MySQL 触发器内部不允许出现 BEGIN...END 块里的 START TRANSACTION、COMMIT 或 ROLLBACK,否则直接报错 ERROR 1305 (42000): SAVEPOINT does not exist 或更常见的 ERROR 1317 (70100): Query execution was interrupted。这是因为触发器属于当前语句事务的一部分,它没有独立事务上下文。
实操建议:
- 历史表写入必须依赖主表操作的事务一致性——主表插入/更新/删除成功,触发器才执行;失败则全部回滚
- 如果需要“失败也不影响主表”,得改用应用层异步写入(比如监听 binlog),而不是触发器
- 避免在触发器里调用存储过程,除非该过程明确不包含事务控制语句
- 测试时务必用真实 DML 操作验证,不要只跑
SELECT或单独调用触发器逻辑(它根本不能被直接调用)
INSERT BEFORE 触发器无法读取 NEW.id(自增主键)
在 BEFORE INSERT 触发器中,NEW.id 是空值或默认值,即使字段定义为 INT AUTO_INCREMENT。MySQL 只有在记录真正插入后才生成自增值,所以这时你拿不到最终的 id。
实操建议:
- 历史存档若需关联原始记录 ID,必须用
AFTER INSERT触发器,此时NEW.id已确定 - 但注意:
AFTER触发器里不能再修改NEW字段(会报错ERROR 1362 (HY000)),只能做只读或写入其他表 - 如果历史表结构和原表完全一致,且想保留原始时间戳,记得在
AFTER INSERT中显式写入NOW()或SYSDATE(),别依赖默认值——触发器执行时刻可能略晚于主表插入时刻
UPDATE 触发器要区分 OLD 和 NEW,别漏判 NULL
判断字段是否变更时,常见错误是写成 IF OLD.col != NEW.col THEN,但当任意一边为 NULL 时,这个表达式永远返回 NULL(即 false),导致逻辑跳过。MySQL 的 != 不做 NULL 安全比较。
实操建议:
- 用
NOT (OLD.col NEW.col)替代,是 MySQL 的 NULL 安全等于运算符 - 如果只关心某些关键字段变动(比如
status或amount),就在触发器里单独判断它们,别无脑全字段比对——性能差,还容易因长度限制或字符集问题出错 - 历史表建议加一个
change_type ENUM('INSERT','UPDATE','DELETE')字段,方便后续查归档原因 - 避免在 UPDATE 触发器里反复读写同一张历史表,尤其高并发场景下容易锁表
DELETE 触发器里 OLD 数据只读,且不能被子查询引用多次
在 BEFORE DELETE 或 AFTER DELETE 触发器中,OLD 是只读的,试图赋值会报错 ERROR 1362 (HY000): Updating of OLD row is not allowed in trigger。更隐蔽的问题是:如果在触发器里用 INSERT ... SELECT 把 OLD 写入历史表,而 SELECT 子句里又对 OLD 引用了两次(比如 SELECT OLD.id, OLD.id + 1),MySQL 会报错 ERROR 1351 (HY000): Can't modify more than one table in a single statement —— 这其实是老版本 MySQL 的解析 bug,但至今仍存在于 5.7 和 8.0.2x 之前的部分小版本。
实操建议:
- 统一用
AFTER DELETE存档,确保主表记录已删干净再落库,避免主从延迟导致“先存档后删除”的幻读 - 写入历史表时,把
OLD字段逐个列出,不用SELECT *,防止字段顺序/数量变化引发隐式错误 - 如果历史表有唯一约束(如
(original_id, deleted_at)),注意deleted_at必须用NOW(3)级别精度,否则毫秒级并发删除可能冲突
最麻烦的不是语法,而是触发器一旦上线,就很难安全地做结构变更——改原表字段类型?历史表也得同步;加 NOT NULL?触发器里没设默认值就会中断写入。上线前一定在影子库压测 DML 链路,别信本地单条 SQL 能过就万事大吉。











