必须用触发器的场景包括:跨表查询(如插入订单前查users表确认用户状态)、依赖old/new对比的状态校验(如禁止修改已发货订单的shipping_address)、需自定义错误提示(如“用户余额不足”)、以及涉及会话变量或外部数据的校验;check约束无法处理这些。

MySQL 8.0 中,复杂业务校验不能只靠触发器硬扛,也不能只用 CHECK 约束一揽子兜底——必须分层:简单、单表、确定性规则交给 CHECK,跨表、状态依赖、需自定义提示的逻辑才进触发器。否则要么校验漏掉,要么性能崩盘。
哪些校验必须用触发器,CHECK 做不了
CHECK 约束在 MySQL 8.0.16+ 虽然支持子查询(带限制),但依然无法处理这些场景:
- 插入订单前查
users表确认status = 'active'——CHECK不允许关联查询其他表 - 更新商品库存时,检查是否存在未支付订单占用该 SKU —— 需要
SELECT ... FROM orders WHERE sku = NEW.sku AND status = 'pending',CHECK不支持 - 禁止将已发货订单的
shipping_address改为NULL,且仅当OLD.status = 'shipped'时生效 —— 涉及OLD/NEW对比和条件分支,CHECK无状态上下文 - 要求错误提示带业务语义,比如
'用户余额不足,无法下单'——CHECK报错固定为Check constraint violated,不可定制MESSAGE_TEXT
触发器里怎么安全调用 CHECK 已覆盖的字段规则
别重复造轮子。如果某字段已有 CHECK (price > 0),触发器里就不要再写 IF NEW.price —— 这不仅冗余,还会让错误堆叠(先报 <code>CHECK 错,再报触发器错),干扰定位。
- 把
CHECK当“前置过滤器”:它拦截所有非法值,触发器只处理通过CHECK后仍需进一步判断的合法值 - 例如:
status字段有CHECK (status IN ('draft', 'published', 'archived')),触发器只需关心IF OLD.status = 'draft' AND NEW.status = 'published' THEN ...的状态流转逻辑 - 确保
CHECK和触发器逻辑不矛盾,比如CHECK (quantity >= 0)存在时,触发器里就别再对负数做特殊处理
BEFORE 触发器中混用 CHECK 与自定义校验的实操要点
所有业务校验必须放在 BEFORE INSERT 或 BEFORE UPDATE 中,且优先复用 CHECK 已验证的部分,再叠加触发器逻辑:
- 用
EXISTS替代COUNT(*) > 0:例如IF NOT EXISTS (SELECT 1 FROM contracts WHERE user_id = NEW.user_id AND status = 'valid') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户无有效合同'; END IF; - 显式处理
NULL:比如IF NEW.age IS NULL OR NEW.age 150 THEN ...,不能只写NEW.age ,否则 <code>NULL会跳过判断 - 避免在触发器里改
NEW字段后又用它参与校验逻辑——顺序混乱易出错;如需标准化,统一在IF判断前完成赋值 - 跨表查询务必走索引:确认
contracts(user_id, status)是联合索引,否则EXISTS也慢成全表扫
容易被忽略的事务与版本陷阱
触发器和 CHECK 共享同一事务上下文,但它们的失败行为不同,这点常被误判:
-
CHECK违反 → 立即终止语句,回滚整条INSERT或UPDATE,不进触发器 - 触发器中
SIGNAL→ 同样回滚,但错误堆栈里会包含触发器位置,方便定位是哪条业务规则卡住 - MySQL 8.0.16+ 才真正支持
SIGNAL;低于此版本(如 8.0.15)仍会报ERROR 1305 (42000): FUNCTION does not exist,不是权限问题,是语法不支持 - 别在触发器里调用含
COMMIT的存储过程——哪怕过程声明为READS SQL DATA,只要内部有事务控制,就会触发ERROR 1442
最常被绕开却最关键的一点:批量操作(如 INSERT INTO t VALUES (),(),())会让触发器逐行执行,此时哪怕每行只多花 0.5ms,1000 行就是半秒延迟。别等上线后才查 EXPLAIN 发现触发器里的 SELECT 没走索引。











