mysql中替代check约束的唯一可靠方式是before触发器配合signal抛异常;校验须在before insert/update中修改new或抛错,after无效;delete仅读old;跨表查询须用exists加索引;禁止更新当前表、调外部服务或复杂计算。

MySQL 中能替代 CHECK 约束实现复杂校验的,只有触发器;但必须用对时机、写法和错误机制,否则不是报错就是静默失效。
BEFORE 触发器才是校验主战场
校验逻辑必须放在 BEFORE INSERT 或 BEFORE UPDATE 里——只有这时才能通过修改 NEW 字段或抛异常阻止写入。放到 AFTER 里就晚了:NEW.status 改不动,SIGNAL 也拦不住已落盘的数据。
- INSERT 场景:只读
NEW,可赋值(如补默认时间)、可校验(如查users表确认用户状态) - UPDATE 场景:
OLD和NEW都可用,但只能改NEW;比如IF OLD.status = 'draft' AND NEW.status = 'published' THEN SET NEW.published_at = NOW(); - DELETE 场景:只读
OLD,适合做关联记录存在性检查(如“不能删还有未完成订单的客户”)
SIGNAL 抛异常是唯一可靠回滚方式
别写 ROLLBACK,它在触发器里直接报 ERROR 1305 (42000);也别用插入不存在表这种野路子,MySQL 5.7+ 必须用 SIGNAL。
- 语法必须完整:
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户余额不足';—— 缺SQLSTATE或引号不匹配都会报错 -
SQLSTATE '45000'是通用自定义错误码,应用层捕获时靠这个区分业务校验失败和其他异常 - MySQL 5.6 及更早版本确实不支持
SIGNAL,但硬升级比维护一堆INSERT INTO nonexistent_table更可持续
跨表查询必须加索引,且优先用 EXISTS
触发器里一句 SELECT COUNT(*) > 0 FROM orders WHERE user_id = NEW.user_id 就可能让批量导入卡住——它会扫全表,还容易锁表。
- 改用
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会直接被 MySQL 拒绝,权限再高也不行
别让触发器变“微型应用层”
触发器不是写业务逻辑的地方。它只该干三件事:字段合法性拦截、跨表存在性校验、状态迁移守门。其余都该下沉。
- 要生成单号?交给应用层或带事务的存储过程,别在触发器里拼字符串
- 要发消息或调 HTTP?不行——破坏事务原子性,且 MySQL 不允许
- 要审计日志?可以写
INSERT INTO audit_log,但目标表不能是当前 DML 正在操作的那张 - 复杂聚合计算(如“用户总消费不能超额度”)?先在应用层算好传进来,触发器只比对
NEW.total
最常被忽略的一点:触发器里的 NEW 值已经是表达式计算后的结果,比如生成列或 JSON 函数输出,不是原始输入值;校验逻辑若依赖原始输入格式,得在应用层做前置处理,别指望触发器能还原。










