触发器导致QPS断崖式下跌,因其将批量操作拆为逐行事务,引发锁竞争、WAL暴增与并行优化失效,且耗时不入慢日志、难以监控。
触发器对系统的影响不能靠EXPLAIN主SQL来判断——它根本不显示触发逻辑。真实影响体现在吞吐量断崖式下跌上。压测时必须分层跑并发:从16线程开始,逐步加到64、128,观察QPS拐点。多数系统在64线程后触发器带来的损耗会从20%跳到40%以上,这不是线性增长,而是锁竞争和WAL暴增的叠加效应。
常见错误现象:slow_query_log里找不到慢SQL,但SHOW ENGINE INNODB STATUS里innodb_row_lock_time_avg飙升;performance_schema.events_statements_history_long中能捞出大量含TRIGGER字样的事件,但耗时不单独计时。
long_query_time = 0抓全量慢日志,再用EXPLAIN FORMAT=TREE确认是否触发器内SQL走了全表扫描performance_schema.data_locks,确认触发器是否因没索引导致锁范围扩大(比如UPDATE stats SET cnt = cnt + 1 WHERE type = NEW.type,而stats(type)无索引)sysbench oltp_insert这种单表脚本测——它不模拟真实字段约束和跨表校验,结果严重乐观一个INSERT INTO orders VALUES (1),(2),(3)本该是1次写入,有AFTER触发器就变成3次独立事务+3次binlog记录+3次锁申请。批量操作时WAL日志量直接翻倍,主从延迟也跟着涨。这不是“多执行几条SQL”那么简单,是事务生命周期被强行拉长、并行优化被废掉。
使用场景:LOAD DATA INFILE导入50万行,理想IO是顺序大块写;触发器存在时,等于执行50万次小事务,I/O随机化,pg_stat_bgwriter(PostgreSQL)或Innodb_os_log_written(MySQL)指标会异常凸起。
pg_stat_activity中backend_start与xact_start时间差,若远大于应用层预期,说明触发器拖住了事务Innodb_rows_updated和Handler_update比值,若接近1:1,说明每行UPDATE都触发了额外操作SELECT COUNT(*) FROM big_table WHERE user_id = OLD.user_id——即使只查一行,没索引就是全表扫,每改一行都扫一遍触发器最危险的不是慢,而是不可见的连锁反应。比如用户表UPDATE触发器去更新统计表,而统计表的UPDATE又触发另一个触发器去写日志表——这已经不是两层嵌套,是调用链爆炸。线上偶发死锁、卡顿,90%查不到源头,因为SHOW PROCESSLIST只显示主SQL,阻塞源藏在触发器里。
容易踩的坑:IF UPDATE(status)没写,导致字段没变也执行触发逻辑;AFTER INSERT里又INSERT INTO同一张表,MySQL默认允许但极易引发无限递归;跨表操作没加NOLOCK或隔离级别不匹配,间隙锁直接锁住整段索引区间。
RECURSIVE_TRIGGERS OFF,并在触发器开头加IF @@NESTLEVEL > 2 RETURN
DECLARE EXIT HANDLER捕获异常,避免触发器失败导致整个事务静默回滚INT vs BIGINT隐式转换会让索引失效)临时禁用比任何理论分析都管用。PostgreSQL可用ALTER TABLE t DISABLE TRIGGER ALL,MySQL设SQL_LOG_BIN=0(仅限从库维护),listmonk这类系统甚至要改schema.sql把AFTER INSERT换成DEFERRED约束。实测100万条导入,启用触发器耗时58分19秒,禁用后18分33秒——不是“有点慢”,是三倍时间成本。
性能损耗不是固定百分比,它随数据量非线性放大:10万条时损耗168%,50万条升到220%,并发10进程时更冲到270%。这背后是锁竞争、CPU饱和、IO队列堆积的综合结果,不是调优能抹平的。
queries.sql定义的锁机制与逐行触发的耦合