SQL触发器能否用来实现复杂的校验规则?

作者:袖梨 2026-07-10
必须用BEFORE INSERT/UPDATE触发器校验,因AFTER执行时数据已落盘,SIGNAL无法回滚;BEFORE可修改NEW或抛错中断,且需用完整SIGNAL语法、避免跨表查COUNT(*)、禁止更新当前表。

能,但必须用对时机、写法和错误机制,否则不是报错就是静默失效。

BEFORE INSERT/UPDATE 才是唯一有效的校验入口

校验目标是“阻止写入”,不是“事后补救”。AFTER 触发器执行时数据已落盘,SIGNAL 无法回滚;ROLLBACK 在触发器里直接报 ERROR 1305 (42000),MySQL 明确禁止事务控制语句。只有 BEFORE INSERTBEFORE UPDATE 能在语句执行前介入,通过修改 NEW 字段或抛异常可靠中断。

  • BEFORE INSERT:只读可写 NEW,适合补默认值(如 SET NEW.created_at = NOW())或查其他表校验(如确认用户状态)
  • BEFORE UPDATEOLD 只读,NEW 可写,必须对比两者才能捕获“从合法改非法”的场景(如 OLD.status = 'shipped' AND NEW.status = 'cancelled' 但未归档发货单)
  • AFTER UPDATE 中尝试 SET NEW.status = 'xxx' 是静默忽略——不报错也不生效,极易踩坑

SIGNAL SQLSTATE 是唯一可靠中断方式

别用 INSERT INTO nonexistent_table 这类野路子,MySQL 5.7+ 必须用 SIGNAL,且语法必须完整:

  • 必须带 SQLSTATE '45000'(通用自定义错误码),缺它会报错
  • MESSAGE_TEXT 要用单引号包裹,比如 SET MESSAGE_TEXT = '用户余额不足'
  • MySQL 5.6 及更早版本确实不支持 SIGNAL,硬升级比维护一堆不可靠的兜底逻辑更可持续

跨表校验必须用 EXISTS + 索引,别碰 COUNT(*)

一句 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 也白搭
  • 禁止在触发器里 UPDATEINSERT 当前表,否则触发 ERROR 1442
  • 跨库只允许 SELECTUPDATE other_db.table 直接被拒绝,权限再高也没用

最常被忽略的是状态迁移校验和多行插入场景:触发器里写 SELECT @name = name FROM inserted 在多行插入时会报“子查询返回多于一个值”;而只校验 NEW.amount 是否合法,会漏掉 OLD.amount 合法但 NEW.amount 非法的变更。这些细节不提前想清楚,上线后问题很难复现和定位。

相关文章

精彩推荐