直接说结论:在生产环境中,触发器的数量越多,出现长事务、死锁以及主从延迟的概率就越大,而且这类问题极具隐蔽性,排查难度极高。
问题不在于“可能引发风险”,而是只要并发量上升、数据规模扩大、业务逻辑稍微复杂化,触发器就会立即拖垮写入性能——更棘手的是,你在慢 SQL 日志里根本看不到触发器究竟执行了什么操作。

触发器会让事务执行时间变得不可预测
触发器的本质是“寄生”于主 DML 事务之内运行,它并不具备独立的事务边界。当一条 INSERT 语句执行完毕后,如果触发器内部的 SELECT 或 UPDATE 尚未完成,整个事务就必须持续保持开启状态——锁无法释放,undo log 不能及时清理,binlog 也会不断累积。这就像你排队办理业务,但前面的人突然掏出三份表格需要填写,整条队伍只能等他一个人处理完毕。
- 常见现象:
SHOW PROCESSLIST显示Updating或Waiting for table metadata lock,但实际卡顿的根源是触发器里那条缺少索引的SELECT COUNT(*) FROM audit_log WHERE ... - 如果在
AFTER UPDATE中更新user_stats表,该表的行锁也会被纳入同一事务,与其他操作路径形成死锁闭环 - 批量导入 10 万行数据时,触发器逐行触发执行 → 事务持续数十秒,
INFORMATION_SCHEMA.INNODB_TRX中的TRX_ROWS_MODIFIED会瞬间飙升
触发器内的 SQL 完全逃逸 EXPLAIN 和慢日志追踪
最令人困扰的是,常规手段根本无法定位触发器引发的问题。EXPLAIN INSERT INTO orders 不会展示触发器内部的任何语句;slow_query_log 仅记录主 SQL,不会标注触发器名称,像 Deadlock found when trying to get lock 这样的错误信息也不会告诉你具体是哪条触发器、哪行代码导致的。这就像家里的水管漏水,但只能看到总水表读数异常,却完全不知道具体哪一段管道出了问题。
- 想要定位触发器的耗时?只能查看
performance_schema.events_statements_history_long,但SQL_TEXT默认会被截断,较长的逻辑需要人工拼接才能还原 SHOW CREATE TRIGGER查看定义需要手动执行,无法集成到监控告警体系- 即使是空触发器,也会带来
0.1–0.3ms的固定开销;一个包含SELECT ... FOR UPDATE的触发器,5ms 就能让平均锁等待时间翻倍
高并发下触发器天然导致写入链路串行化
MySQL 触发器的执行机制是行级、同步且逐行调用的。1 条 INSERT 就会触发 1 次,1000 行数据就会触发 1000 次——无法异步执行、不能批量处理、也无法绕开事务边界。在高并发场景下,这相当于把一条单车道硬生生变成了单行道,所有请求都必须排队依次通过。
LOAD DATA INFILE带触发器时,性能衰减呈线性增长,索引优化完全失效- 多个触发器争抢同一张统计表(如
user_summary)的主键时,会高概率出现Deadlock found when trying to get lock - 当触发器数量超过几十个时,
metadata lock等待时间会明显上升,甚至ALTER TABLE操作都可能被阻塞
替代方案比“优化触发器”更加切实可行
与其费力优化触发器,不如换个思路来解决问题。如果确实需要自动同步或审计功能,应用层封装、GENERATED COLUMN、BINLOG 解析(如 Canal)等方案远比硬塞触发器更加可靠。
- 更新时间戳?使用
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,完全不需要触发器 - 审计日志?不要在
AFTER INSERT里写INSERT INTO audit_log,改用 Canal 抽取 binlog 异步落库即可 - 统计类字段?采用应用层双写或定时任务覆盖更新,避免每行变更都触发重复计算
- 如果非用不可?只允许
BEFORE INSERT/UPDATE做简单赋值,禁用NOW()、RAND(),单个触发器最多包含 1 条写入操作,目标表不能是当前表
最麻烦的从来不是触发器做了什么,而是它做了什么你根本无从知晓;最危险的也不是某条 SQL 执行缓慢,而是整条写入链路被它无声无息地卡住——等你发现问题时,TPS 已经腰斩,主从延迟突破百秒,而 slow log 还在安静地只记录那条“看起来很快”的 INSERT。
