触发器在批量update中必然逐行执行,无法并行或跳过;应禁用触发器,改用生成列、应用层传参或异步处理,必须保留时仅允许无副作用的before触发器。

触发器在批量UPDATE里被逐行调用,根本停不下来
MySQL 对 UPDATE t SET x = 1 WHERE id IN (1,2,3,...,10000) 这类语句,不会“整体触发一次”,而是对每一行单独执行一遍触发器逻辑。哪怕触发器只有一行 NEW.updated_at = NOW(),也会被调用 10000 次——串行、不可跳过、无法并行。
常见错误现象包括:SHOW PROCESSLIST 中大量线程卡在 Updating 状态;慢查询日志里同一条 UPDATE 反复出现,每次耗时稳定在几毫秒以上;INFORMATION_SCHEMA.PROFILING 显示触发器逻辑占总耗时 70%+。
特别注意:NOW() 在高并发下会因系统时钟函数争用导致微小延迟累积;若触发器里还含 SELECT 或 READS SQL DATA 类自定义函数,单次开销可能从 0.3ms 拉到 8ms+,10000 行就是 80 秒。
禁用触发器比优化它更有效
很多团队花时间给触发器加索引、拆逻辑、缓存查询结果,但问题根源不在写得不够好,而在于它本就不该承担批量场景下的逻辑分发职责。MySQL 触发器的设计定位是“轻量、确定、单行响应”,不是“业务协调中枢”。
真正有效的绕过方式是让 SQL 本身不走触发器路径:
- 用
INSERT ... ON DUPLICATE KEY UPDATE替代UPDATE:把目标数据先写入临时表,再通过该语法完成,全程不触发任何触发器 - 把
updated_at这类自动填充字段改用生成列:updated_at DATETIME AS (NOW()) STORED(MySQL 8.0+ 支持),写入时自动计算且不走触发器 - 审计字段如
created_by必须由应用层显式传参,不要依赖@user_id或触发器读取 session 变量 - 统计类更新(如订单数累加)彻底移出 DB,改用 BINLOG 解析(Canal/Maxwell)或应用层异步任务处理
必须保留触发器时,只允许纯计算的 BEFORE 触发器
如果因强审计要求无法移除(比如金融系统),那就把触发器压缩到只剩最底线能力:
- 仅支持
BEFORE INSERT和BEFORE UPDATE,禁用所有AFTER类型 - 体内禁止任何
SELECT、UPDATE、INSERT、DELETE操作 - 禁止调用
READS SQL DATA或MODIFIES SQL DATA的自定义函数 - 字段赋值仅限确定性表达式,例如
NEW.ts = UNIX_TIMESTAMP(),不用NOW() - 禁止
IF/CASE分支以外的逻辑分支(比如循环、异常捕获)
哪怕你用了 LIMIT 分批执行 UPDATE,只要表上有触发器,每一批里的每一行仍会触发一次——批次越小,上下文切换开销越重。
别指望 EXPLAIN 看出触发器耗时
EXPLAIN FORMAT=TREE 只展示 DML 本身的执行计划,触发器是在语句执行后由存储引擎层同步调用的“事后动作”,不参与 Cost Model 计算,所以:
- 你无法用
EXPLAIN预估触发器耗时,只能靠performance_schema.events_statements_history_long查具体执行时间 - MySQL 8.0 默认开启
events_statements_history_long,采集开销反而比 5.7 高,测试前务必执行:UPDATE performance_schema.setup_consumers SET ENABLED = 'NO' WHERE NAME LIKE 'events_statements_%'; - 升级到 8.0 后元数据加载更快(事务性数据字典),但如果你没关掉性能采集,实际表现可能更慢
真正卡住你的,往往不是某一行逻辑写错了,而是你默认接受了“触发器该这么用”这个前提——而它在批量场景下,从来就不该被启用。











