必须用触发器的场景只有一类:逻辑必须“不可绕过”——哪怕应用层、DBA直连、ORM或mysql -e执行SQL,也强制执行;如AFTER INSERT写审计日志、BEFORE INSERT补字段、BEFORE UPDATE校验数据、级联更新需原子性事务保障。

必须用触发器的场景,只有一类:逻辑必须“不可绕过”——哪怕应用层、DBA直连、ORM、甚至 mysql -e 执行 SQL,也得强制执行。其他情况,优先用存储过程。
数据变更时强制审计或补全,绕不开触发器
当你要确保某件事“只要数据变了,就一定发生”,且不能被任何写法跳过,就得用触发器。
- AFTER INSERT ON
orders自动写入order_audit表:哪怕有人用脚本批量导入、用 Navicat 直接粘贴 SQL,日志也必留 - BEFORE INSERT ON
users补全created_at和status字段:避免空值进入主表,不依赖应用传参或 ORM 默认值 - BEFORE UPDATE ON
products校验price > 0:失败则整个 UPDATE 被拒绝,不是事后告警
注意:BEFORE 触发器能改 NEW 行,AFTER 只能读;但两者都禁止修改当前触发表本身,否则报 ERROR 1442。
级联状态同步必须原子性,触发器是唯一选择
比如用户停用时,session 表必须同步失效,且这个动作不能和主表更新分开提交——它得和原始 UPDATE 在同一个事务里完成。
- UPDATE
usersSETis_active = 0→ BEFORE/AFTER 触发器自动 UPDATEsessionsWHEREuser_id = OLD.id - 不能靠存储过程分两步做:先改 users,再调
CALL sync_session_status(),中间可能失败或被并发干扰 - 触发器天然共享事务上下文,失败即回滚,这是存储过程做不到的隐式保障
存储过程更适合复用、调试、带分支的业务封装
如果你的需求是“多处调用”“要传参”“要返回结果”“要加 if/else”,那基本不该用触发器。
-
CALL freeze_user(123, 'security_breach')内部可做多步:更新状态、删 session、归档订单、发异步任务 —— 这些逻辑没法塞进单个触发器体里 - 需要校验导入文件格式?用存储过程接收
IN file_path VARCHAR(255),内部 SELECT + CASE 判断,异常写import_error_log - 要算月度营收?
CALL calc_monthly_revenue(202606, @total),之后查@total即可 —— 触发器无法返回变量
存储过程里禁止出现 COMMIT 或 ROLLBACK(除非声明为 DETERMINISTIC 并开启 log_bin_trust_function_creators),否则主事务会崩。
触发器调用存储过程:能做,但边界极窄
可以拆分工序(守门 + 干活),但极易踩坑。
- 只允许在
BEFORE或AFTER中CALL,不能在FOR EACH ROW循环里反复调用,否则性能雪崩 - 被调用的存储过程绝对不能碰当前触发表,比如
users的触发器里UPDATE users会直接报ERROR 1442 - 整个链路共用一个事务:如果存储过程中插入日志表失败(如外键不匹配),原始 INSERT 也会回滚 —— 这点常被忽略
真正难的不是语法,而是判断“这件事到底该由数据库守门,还是由应用/存储过程调度”。一旦把本该由应用控制的流程塞进触发器,就会变成黑盒、难测、难调、难 rollback。

















