必须在before触发器中用signal抛异常才能有效拦截非法数据,因其在数据写入前执行,可回滚事务并阻止脏数据入库;after触发器数据已落盘,抛异常无法撤回变更。

必须用 SIGNAL SQLSTATE 在 BEFORE 触发器里抛异常,否则业务规则校验无效——AFTER 触发器无法中止已执行的操作,也不该用来做校验。
为什么只能在 BEFORE 触发器里用 SIGNAL
触发器运行在父语句的事务上下文中,AFTER INSERT 或 AFTER UPDATE 执行时,数据早已写入磁盘。此时再抛 SIGNAL,只会让客户端收到错误,但变更已生效,事务无法回滚原操作。
-
BEFORE INSERT:可读写NEW,适合字段赋值、范围检查、查重拦截 -
BEFORE UPDATE:可改NEW.column_name(影响最终写入值),用OLD做比对 -
BEFORE DELETE:只读OLD,可用于权限或依赖校验(如“不可删除有订单的用户”) -
AFTER类型触发器里写SIGNAL语法虽不报错,但实际起不到拦截作用,属于典型误用
正确写法与常见拼写错误
SIGNAL SQLSTATE 不是“写了就生效”,漏掉任一要素都会导致静默失败或报错。
- SQLSTATE 必须是 5 位字符串,前两位为字母或数字,推荐固定用
'45000'(用户自定义通用错误) - 必须显式写
SET MESSAGE_TEXT = 'xxx',缺了这句会报ERROR 1337 (42000) - 消息内容不能含未处理的
NULL:避免CONCAT('ID ', NEW.id, ' 无效'),若NEW.id为NULL,整条SIGNAL被跳过 - 字符串必须用单引号包裹,
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不能为负'是合法写法;双引号或无引号会语法报错 - MySQL 8.0.16+ 支持
MESSAGE_TEXT最长 128 字符,超长自动截断,不提示
跨表校验时容易踩的坑
想查另一张表判断业务逻辑(比如“同一用户当天订单数 ≤ 3”),看似合理,实则极易引发性能和锁问题。
- 禁止在触发器里
SELECT当前被操作的表(如BEFORE INSERT ON users里又SELECT FROM users),会直接报ERROR 1442 (HY000) - 查关联表时,务必确保被查字段有索引,否则批量插入/更新时全表扫描拖垮写入性能
- 避免在触发器里调用含事务逻辑的存储过程,哪怕它声明为
READS SQL DATA,也可能隐式开启事务导致冲突 - 不要用
SELECT ... INTO @var做存在性判断:查不到会触发ERROR 1329 (02000),且无法捕获,应改用EXISTS+IF
应用层怎么真正捕获这个错误
命令行里看到 ERROR 1644 (45000): 余额不能为负 并不等于应用里能正常处理——很多驱动默认只暴露 errno 和简短 sqlMessage。
- Python 的
pymysql需检查exc.args或exc.sqlstate字段,不能只靠str(exc) - Node.js 的
mysql2中,err.sqlState === '45000'才代表是触发器主动抛出的业务错误 - PHP 的 PDO 需启用
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,否则SIGNAL可能被忽略成静默失败 - 事务场景下,即使触发器抛了
SIGNAL,上层也得显式ROLLBACK,别依赖 ORM 自动回滚(老版 Django 就不保证)
最常被忽略的一点:触发器里的校验逻辑一旦涉及多表查询或函数调用,就不再是“轻量级兜底”,而成了写入路径上的性能瓶颈。宁可用应用层预检 + 数据库唯一索引兜底,也别把复杂业务规则硬塞进触发器。











