根本原因是触发器将批量dml强制转为逐行串行执行:每行更新均独立开事务、记日志、加锁、执行逻辑,且禁用并行优化;其耗时不单独记录、不可调试、锁范围易放大,导致性能断崖下跌。

触发器在 SQL 中被称作“性能杀手”,根本原因不是它功能强,而是它把原本能批量、并行、优化的 DML 操作,强行拖进逐行、同步、事务绑定的窄巷里——而且你还看不见它在拖。
为什么批量 UPDATE 变成 N 次串行小事务
MySQL/PostgreSQL/SQL Server 的行级触发器(FOR EACH ROW)对每行变更都单独执行一次函数调用。这意味着:
-
UPDATE users SET status = 'active' WHERE region = 'CN'影响 5000 行 → 触发器被执行 5000 次,不是 1 次 - 每次触发都开新事务上下文、记 WAL / binlog、加锁、跑逻辑、等返回——哪怕逻辑只有一行
SET NEW.updated_at = NOW() - InnoDB 的
BATCHED_KEY_ACCESS优化在有触发器时自动失效;PostgreSQL 的parallel_tuple_cost也失去作用 - 实测:带轻量触发器的批量更新,
innodb_row_lock_time_avg上升 3–8 倍,但慢日志里查不到对应语句
为什么触发器里的 SELECT 会扫全表
触发器内写的 SELECT 看似简单,但极易因隐式转换或写法问题跳过索引:
-
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)缺失索引,执行计划显示type: ALL - 这类查询在应用层调一次是隐患,在触发器里每行调一次就是雪崩
为什么锁范围和持有时间被触发器放大
主 SQL 本只锁目标行,但触发器一介入,锁就可能升级、扩散、延时释放:
- 主语句锁
orders.id = 123,触发器里执行UPDATE stats SET cnt = cnt + 1 WHERE type = NEW.type—— 若stats(type)无索引,InnoDB 升级为表级锁 - 这个锁要等到整个事务提交才释放,而事务结束时间由触发器决定;若触发器里有远程调用或慢查询,锁就挂住几秒
- 并发事务互相触发对方表:A 表 UPDATE → 触发 B 表 INSERT → B 表又有触发器 UPDATE A 表 →
Deadlock found when trying to get lock几乎必然发生 -
SHOW ENGINE INNODB STATUS显示trx_query是主 SQL,但阻塞源藏在触发器里,排查时容易误判
为什么触发器耗时不进监控也不可调试
它的执行完全嵌入 DML 生命周期,没有独立生命周期,也没有标准观测入口:
- MySQL 没有 SQL Profiler;
EXPLAIN不展示触发器路径;slow_query_log记录的是主语句耗时,含触发器部分被合并进去 - 想定位耗时,得从
performance_schema.events_statements_history_long里捞EVENT_NAME LIKE '%trigger%'的记录,再人工拼接SQL_TEXT(该字段默认截断) - 无法设断点、不能打印变量、
SIGNAL会直接中断事务、SELECT会报错;唯一“调试”方式是往日志表硬插记录,还可能因日志表没索引进一步拖慢系统 - 多个触发器叠加(
BEFORE+AFTER+ 多表联动)时,错误传播路径模糊,ERROR 1422这类报错只说“不允许提交”,不指明是哪个触发器、哪一行出的问题
真正难处理的,从来不是语法怎么写,而是默认把它当成“轻量钩子”之后,才发现它早已悄悄接管了整条数据链路的节奏与瓶颈。











