必须用 before 而不是 after 触发器,因为校验目标是阻止写入而非事后补救;after 执行时数据已落盘,signal 无法回滚;before 可在语句执行前介入,通过 signal sqlstate 抛错可靠中断。

只能用 BEFORE INSERT / BEFORE UPDATE 触发器 + SIGNAL 抛错,才能真正拦截非法数据;AFTER 或 ROLLBACK 都无效,且 CHECK 约束无法跨表、不能调用子查询(8.0.16+ 有限支持但不推荐替代触发器)。
为什么必须用 BEFORE 而不是 AFTER?
因为校验目标是“阻止写入”,不是“事后补救”。AFTER 触发器执行时,INSERT/UPDATE 已完成,哪怕你 SIGNAL,数据也已落盘。BEFORE 才能在语句执行前介入。
-
BEFORE INSERT:只读NEW,可赋值(如补created_at)、可校验(如查users表确认状态) -
BEFORE UPDATE:OLD和NEW都可读,但只能改NEW字段(如限制状态流转:IF OLD.status = 'draft' AND NEW.status = 'published' THEN SET NEW.published_at = NOW();) -
AFTER UPDATE中修改NEW.status是静默忽略的——不报错,也不生效,极易踩坑
SIGNAL 是唯一可靠中断方式,别碰 ROLLBACK
触发器里写 ROLLBACK 会直接报 ERROR 1305 (42000),这不是函数缺失,而是 MySQL 明确禁止事务控制语句。正确做法是抛异常让外层自动回滚。
- 语法必须完整:
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户余额不足'; - 缺
SQLSTATE、引号不匹配、或 MySQL 版本 - MySQL 5.6 及更早版本不支持
SIGNAL,靠INSERT INTO nonexistent_table是野路子,不可靠、难调试、不建议
跨表校验必须用 EXISTS + 索引,别写 COUNT(*)
一句 SELECT COUNT(*) > 0 FROM orders WHERE user_id = NEW.user_id 在批量导入时可能卡死——它扫全表,还容易锁表。而 EXISTS 遇到第一条匹配就返回,性能差一个数量级。
- 写成:
EXISTS (SELECT 1 FROM orders WHERE user_id = NEW.user_id AND status = 'pending') -
user_id字段必须有索引,否则EXISTS也白搭 - 禁止在触发器里
UPDATE或INSERT当前表,否则触发ERROR 1442 - 跨库只允许
SELECT,UPDATE other_db.table直接被拒绝,权限再高也没用
UPDATE 场景下容易漏掉 OLD 和 NEW 的双向校验
只校验 NEW.amount 是否合法,会忽略“从合法值改为非法值”的情况。比如订单状态从 'shipped' 改为 'cancelled',但关联发货单未归档——这需要对比 OLD.status 和 NEW.status 才能捕获。
- 典型状态机校验:
IF OLD.status = 'paid' AND NEW.status = 'refunded' THEN ... - 字段是否被置空也要比对:
IF OLD.shipping_address IS NOT NULL AND NEW.shipping_address IS NULL THEN SIGNAL ... - 对可能为
NULL的字段判断,必须用IS NULL,别用= NULL(永远返回 false)
真正难的不是写触发器,而是控制它的边界:它不该做复杂计算、不该调外部服务、不该更新当前表、也不该承担本该由应用层处理的状态协调。一旦逻辑膨胀,就变成难以测试、无法回滚、拖慢批量操作的隐性瓶颈。











