触发器在批量update中逐行执行,导致高并发下严重性能瓶颈;应禁用触发器,改用原生sql语法、生成列或应用层处理。

触发器在批量UPDATE里被逐行调用,根本停不下来
MySQL 对 UPDATE t SET x = 1 WHERE id IN (1,2,3,...,10000) 这类语句,不会“整体触发一次”,而是对每一行单独执行一遍触发器逻辑。哪怕触发器只有一行 NEW.updated_at = NOW(),在高并发下也会因系统时钟函数争用累积延迟;若含 SELECT 或自定义函数,单次开销可能从 0.3ms 拉到 8ms+,10 万行就是 800 秒。
常见错误现象包括:SHOW PROCESSLIST 中大量线程卡在 Updating 状态;慢查询日志里同一条 UPDATE 反复出现,每次耗时稳定在几毫秒以上;INFORMATION_SCHEMA.PROFILING 显示触发器逻辑占总耗时 70%+。
触发器里写 JOIN 或游标,等于主动加锁+放大 IO
触发器是行级同步执行的,每插入/更新一行就完整跑一遍逻辑。比如 AFTER INSERT 里写 SELECT SUM(amount) FROM order WHERE user_id = NEW.user_id,那每下一单就触发一次全表扫描——不是“慢一点”,而是单条写入延迟从 0.5ms 拉到 20ms+,1000 QPS 就直接卡住。
游标更危险:每次触发都重新编译、结果集全加载进内存、FETCH 必须串行等待,还极易引发 Waiting for table metadata lock。监控指标如 Created_tmp_disk_tables 突增、memory/sql/THD::main_mem_root 占用飙升,基本就是游标在吃内存。
-
EXPLAIN不显示触发器逻辑,但你可以把触发器里的每条SELECT或UPDATE单独拎出来EXPLAIN,重点看key和rows - JOIN 字段类型必须严格一致(比如
BIGINT和INT关联会失效),否则索引白建 - 游标内 SQL 没走索引的典型表现:
EXPLAIN返回type: ALL,或Extra出现Using join buffer
禁用比优化更有效,因为设计定位本就不支持批量
MySQL 触发器的设计定位是“轻量、确定、单行响应”,不是“业务协调中枢”。很多团队花时间给触发器加索引、拆函数、缓存查询结果,但问题根源不在写得不够好,而在于它本就不该承担批量场景下的逻辑分发职责。
真正有效的办法是让 SQL 本身不走触发器逻辑:INSERT ... ON DUPLICATE KEY UPDATE、INSERT ... SELECT、临时表 JOIN 更新等原生语法,本身就绕过了触发器执行路径——只要没显式写 UPDATE,就不会触发。
- 字段自动填充(如
updated_at)改用生成列:updated_at DATETIME AS (NOW()) STORED(MySQL 8.0+) - 审计字段(如
created_by)必须由应用层显式传参,不要依赖@session_var或触发器读取连接上下文 - 统计类变更(如订单数累加)彻底移出 DB,改用 BINLOG 解析(Canal/Maxwell)或应用层异步任务
必须保留触发器时,只允许最简化的 BEFORE + 纯计算
如果因强审计要求无法移除(例如金融系统),那就把触发器压缩到只剩最底线能力:仅 BEFORE INSERT / BEFORE UPDATE,且体内严禁任何副作用。
硬约束包括:禁止任何 SELECT、UPDATE、INSERT、DELETE 操作;禁止调用 READS SQL DATA 或 MODIFIES SQL DATA 的自定义函数;禁止 IF/CASE 分支以外的逻辑分支(比如循环、异常捕获);字段赋值仅限确定性表达式,如 NEW.ts = UNIX_TIMESTAMP(),不用 NOW()。
分批更新时仍要警惕触发器残留影响——即使你用了 LIMIT 分批执行 UPDATE,只要表上有触发器,每一批里的每一行仍会触发一次。这不是“批次小就安全”,而是“批次越小,触发器调用次数越多,上下文切换开销越重”。











