MySQL触发器在批量UPDATE时必然拖慢接口,因其强制逐行执行、无法跳过或并行;1000行更新即触发1000次完整上下文切换与开销,空触发器亦有0.1–0.3ms固定延迟,含系统函数时单次超0.8ms。

MySQL触发器在批量UPDATE时拖慢接口响应,不是因为写法不对,而是它根本无法跳过、无法合并、无法并行——1000行更新,就强制执行1000次触发器逻辑,哪怕只有一行NEW.updated_at = NOW()。
为什么批量UPDATE会变成N次串行小事务
MySQL对UPDATE t SET x=1 WHERE id IN (1,2,3,...,1000)这类语句,不会整体触发一次,而是为每一行匹配结果单独调用一次触发器。每次调用都包含完整上下文切换、权限检查、变量初始化、binlog写入等开销。
- 哪怕空触发器,实测也带来
0.1–0.3ms固定延迟;含NOW()或RAND()时,高并发下系统函数争用会让单次延迟升至0.8ms+ -
INFORMATION_SCHEMA.PROFILING显示触发器逻辑常占总耗时70%+,但slow_query_log里只记录主语句,且反复出现同一SQL,每次耗时稳定在几毫秒 -
SHOW PROCESSLIST中大量线程卡在Updating或Executing trigger状态,而非Query或Sending data
触发器内SELECT为何等于每行扫一次全表
触发器里的SELECT看似轻量,但极易因隐式类型转换、函数包装或缺失索引,在每次调用时都退化为全表扫描——这不是“偶尔慢”,是N次确定性全表扫描。
- 例如
SELECT balance FROM accounts WHERE user_id = NEW.user_id:若accounts.user_id是BIGINT而NEW.user_id是INT,类型不匹配导致索引失效 -
WHERE UPPER(email) = UPPER(NEW.email):字段被函数包裹,索引完全无法使用 -
SELECT COUNT(*) FROM logs WHERE user_id = OLD.user_id:若logs(user_id)无索引,EXPLAIN返回type: ALL,且该SQL必须从performance_schema.events_statements_history_long中单独捞出才能分析
锁范围和持有时间被触发器悄悄放大
主SQL本只锁目标行,但触发器一介入,锁就可能升级、扩散、延时释放——而这个锁要等到整个事务提交才释放,事务结束时间却由触发器决定。
-
BEFORE UPDATE里查配置表:SELECT value FROM config WHERE key = 'rate_limit'→ 每次更新都重查,O(1)变O(n) -
AFTER INSERT里更新用户表:UPDATE users SET last_order_time = NOW() WHERE id = NEW.user_id→ 主事务锁订单行,触发器又去锁users行;另一条路径反着来,Deadlock found when trying to get lock几乎必然发生 -
SHOW ENGINE INNODB STATUS显示trx_query是主SQL,但阻塞源藏在触发器里;innodb_row_lock_time_avg突增3–8倍,却查不到对应慢SQL
禁用比优化更有效,绕过比调试更现实
花时间给触发器加索引、缓存查询、拆函数,解决不了本质问题:MySQL触发器的设计定位是“轻量、确定、单行响应”,不是“业务协调中枢”。它天生不适合批量场景。
- 用
INSERT ... ON DUPLICATE KEY UPDATE替代UPDATE:把目标数据先写入临时表,再通过该语法完成,全程不触发任何触发器 - 把
updated_at这类字段改用生成列:updated_at DATETIME AS (NOW()) STORED(MySQL 8.0+),写入时自动计算且不走触发器 - 审计字段如
created_by必须由应用层显式传参,不要依赖@user_id或触发器读取session变量 - 必须保留触发器时,只允许纯计算的
BEFORE触发器:禁止SELECT、禁止UPDATE/INSERT/DELETE、禁止调用READS SQL DATA函数、仅限确定性表达式如NEW.ts = UNIX_TIMESTAMP()
真正难绕开的点是:触发器逻辑不进慢日志、不进EXPLAIN、不暴露在监控指标里。你看到的只是“这条UPDATE变慢了”,却很难一眼定位到慢在触发器里那条没索引的SELECT——它藏得深,还甩不掉。


















