MySQL触发器必然拖慢INSERT,单行延迟可从0.3ms升至10ms以上,批量1000行达10秒起步;其串行执行机制、逐行调用、额外IO与锁争用导致性能瓶颈,优化无效,应禁用或改用生成列、异步队列等替代方案。

MySQL触发器必然拖慢INSERT,不是“会不会”,而是“慢多少”——只要触发器里有一行SELECT、一个NOW()、一次跨表写入,单行插入延迟就可能从0.3ms跳到10ms以上;批量1000行,就是10秒起步。这不是配置能调优的问题,是机制决定的。
为什么触发器会让INSERT变成串行瓶颈
MySQL对INSERT INTO t VALUES (), (), ()这类语句,不会整体触发一次,而是对每一行单独执行触发器逻辑。哪怕触发器只有SET NEW.created_at = NOW(),10万行就是10万次系统时钟调用+10万次上下文切换+10万次事务内日志写入。
-
AFTER INSERT里写日志表,会强制多一次磁盘IO,且无法被批量合并 -
BEFORE INSERT中SELECT balance FROM accounts WHERE user_id = NEW.user_id,若accounts.user_id没索引,就是全表扫描+间隙锁,直接阻塞其他并发插入 - 多个触发器嵌套时,执行顺序不可控,
EXPLAIN完全不展示其内部SQL,你优化了主语句,却对里面三条UPDATE一无所知 - 空触发器也有0.1–0.3ms固定开销:权限检查、解析、上下文切换全得走一遍
哪些触发器操作必须立刻删掉
以下行为只要出现一次,就足以让写入延迟失控,且任何索引、参数、硬件升级都救不了:
-
SELECT查询其他表(尤其是没复合索引的WHERE status = NEW.status AND created_at > ...) -
UPDATE或INSERT INTO ... SELECT写入关联表(比如订单插入后更新客户积分) - 调用自定义函数(如
GET_USER_RANK()),每次调用都可能触发隐式子事务 - 在
AFTER INSERT中反向UPDATE同一表的汇总行(极易与并发事务死锁) - 使用
NOW()、UUID()、RAND()等非确定性函数(破坏主从一致性,求值开销不可控)
真正能留下的触发器只做四件事
符合“无查询、无跨表、无函数、无分支”四不原则的逻辑,才配留在数据库侧:
-
SET NEW.phone = REPLACE(NEW.phone, ' ', '')(纯字符串处理) -
SET NEW.created_at = NOW()(仅限binlog_format = ROW,且确认不用于主从复制场景) - 空值校验:
IF NEW.email IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'email required'; END IF; - 生成列替代方案更稳:
created_at DATETIME AS (NOW()) STORED(MySQL 8.0+)
比优化更有效的做法:临时禁用或彻底绕过
禁用触发器比重写逻辑快得多,也安全得多:
- MySQL 8.0.19+ 支持:
ALTER TABLE orders DISABLE TRIGGER ins_order_log,批量导入完再ENABLE - 禁用后对比
innodb_row_lock_waits和io_stall_write_ms是否骤降,就能确认是不是触发器拖垮了IO - 真要保留审计逻辑,改用
INSERT INTO trigger_queue (id, table_name) VALUES (NEW.id, 'orders')——一行、无条件、无子查询,再由外部消费者异步处理 - 字段自动填充优先用生成列或默认约束,别依赖触发器;业务联动(如发通知、扣积分)必须交还应用层,失败可重试、可监控、可补偿
最常被忽略的一点:你花三天给触发器加索引、拆函数、缓存结果,不如花三分钟执行DISABLE TRIGGER然后压测。性能没改善?说明问题不在触发器写得够不够好,而在它本就不该出现在那里。



















