只能在 before 阶段抛 signal,因为其执行于数据写入前,可中断语句并触发自动回滚;after 阶段数据已落库,signal 无法阻止主表写入,尤其在 myisam 等非事务引擎下完全不可回滚。

必须用 BEFORE INSERT 或 BEFORE UPDATE 触发器配合 SIGNAL,其他方式(如 ROLLBACK、AFTER 触发器、插入不存在表)要么报错,要么拦不住已写入的数据。
为什么只能在 BEFORE 阶段抛 SIGNAL?
校验的目标是“不让非法数据落库”,不是“事后补救”。AFTER 触发器执行时,INSERT/UPDATE 已完成,哪怕你 SIGNAL 报错,主表记录也已写入——尤其在非事务引擎(如 MyISAM)下完全无法回滚。BEFORE 阶段才能在数据写入前中断语句,触发自动回滚。
-
BEFORE INSERT:只读NEW,可赋值(如补NOW())、可校验(如查用户状态) -
BEFORE UPDATE:可对比OLD.status和NEW.status,实现状态流转控制(如禁止从shipped退回pending) -
BEFORE DELETE不适合做“禁止删除”——用外键或权限更安全;真要拦,只适合检查关联记录是否存在
SIGNAL 写法和常见错误
SIGNAL 是唯一合法的中断方式,但语法稍有不慎就报错。MySQL 5.7+ 支持,5.6 及更早版本只能用不推荐的 hack 方式(如 INSERT INTO nonexistent_table)。
- 必须写全:
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx';—— 缺引号、漏分号、SQLSTATE 错位都会失败 - 别用自定义 SQLSTATE(如
'AB123'),应用层难捕获;统一用'45000' - 拼接动态信息用
CONCAT():SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = CONCAT('金额超限:', NEW.amount); - 所有涉及
NEW.field的比较,先判 NULL:IF NEW.status IS NOT NULL AND NEW.status != 'active' THEN,否则条件恒为 FALSE
跨表校验怎么写才不拖慢数据库?
触发器里 SELECT 查其他表是刚需,但也是性能雷区。一句没走索引的查询,可能让批量导入卡死。
- 禁用
COUNT(*) > 0,改用EXISTS (SELECT 1 FROM ...),并确保 WHERE 条件字段有索引 - 例如校验用户是否活跃:
IF NOT EXISTS (SELECT 1 FROM users WHERE id = NEW.user_id AND status = 'active') THEN→ 要求users(id, status)有联合索引 - 禁止在触发器里
UPDATE或INSERT当前正在操作的表,否则报ERROR 1442 - 别调用含事务语句(
COMMIT/ROLLBACK)的存储过程,会直接触发ERROR 1305
什么情况下不该用触发器?
触发器不是业务逻辑容器。它只该干三件事:字段合法性拦截、跨表存在性校验、状态迁移守门。其余都该下沉。
- 要生成单号、调 HTTP、发消息?不行——破坏事务原子性,MySQL 也不允许
- 涉及多表 JOIN 判断、幂等检查、复杂状态机?交给应用层,触发器只做原子性兜底(比如防止并发重复扣款时,用
SELECT ... FOR UPDATE+ 触发器双重保险) - MySQL 8.0.16+ 的
CHECK约束已支持简单子查询,单表规则优先用它,比触发器轻量得多
真正容易被忽略的是:上线前必须用真实 INSERT/UPDATE 语句验证,而不是只测 SELECT;LOAD DATA INFILE 或禁用 sql_log_bin 时,触发器默认不执行。











