before insert/update是唯一能阻止非法数据落盘的校验入口,通过signal sqlstate '45000' set message_text可主动中断并回滚操作;after无法拦截已写入数据,delete仅能读old用于关联检查,且严禁修改触发表。

MySQL 中唯一能强制执行跨表、状态机、多字段联动等复杂业务逻辑的机制,就是 BEFORE 触发器配合 SIGNAL;CHECK 约束做不到的事,才轮到触发器上——但用错时机或写法,它就变成静默失效或报错源头。
为什么 BEFORE INSERT/UPDATE 是唯一有效的校验入口
BEFORE 是唯一能阻止非法数据落盘的时机。放在 AFTER 里改 NEW.status 会被忽略,SIGNAL 也拦不住已写入的数据;DELETE 场景只能读 OLD,适合做关联存在性检查(比如“不能删还有未完成订单的客户”)。
- INSERT:只读写
NEW,可补默认值(如SET NEW.created_at = NOW()),也可查其他表校验 - UPDATE:
OLD和NEW都可用,但只能改NEW字段;例如状态迁移时,IF OLD.status = 'draft' AND NEW.status = 'published' THEN SET NEW.published_at = NOW() - 别在
AFTER里做校验——它不拦截、不回滚、不报错,只执行完事
SIGNAL 抛异常才是唯一可靠的中断方式
想让插入/更新失败并回滚,必须用 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx'。写 ROLLBACK 会直接报 ERROR 1305 (42000);插不存在表这种野路子在 MySQL 5.7+ 已不可靠,且难定位。
-
SQLSTATE '45000'是通用自定义错误码,应用层靠它区分业务校验失败和其他异常 - 引号必须匹配:
'45000'和'用户余额不足'都要用单引号,缺一个就语法报错 - MySQL 5.6 及更早版本确实不支持
SIGNAL,但硬升级比维护一堆INSERT INTO nonexistent_table更可持续
跨表查询必须用 EXISTS + 索引,否则批量操作就卡死
一句 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 值,会忽略字段从合法改非法(比如 status 从 'active' 改为 'deleted' 但关联记录还在);只校验 OLD,又捕获不到新增违规。
- 典型场景:订单状态从
'shipped'改为'cancelled',得先查order_items是否为空 - PostgreSQL 用户注意:
OLD.status在INSERT里根本不存在,运行时报错record "old" has no field "status",必须用IF TG_OP = 'UPDATE' THEN包裹 - MySQL 用户注意:
SET OLD.status = 'archived'这种赋值会被静默忽略,不报错也不生效——这是最难排查的坑之一
触发器不是微型应用层,它只该干三件事:字段合法性拦截、跨表存在性校验、状态迁移守门。复杂计算、外部调用、日志写入这些,都得剥离出去——否则性能瓶颈和事务一致性风险会立刻浮现。











