触发器中执行复杂查询会严重拖垮写入性能,因其同步逐行执行、无法优化、易引发锁争用与死锁,且难以监控调试;应避免在触发器中做JOIN、聚合或未索引查询,改用应用层预处理、冗余字段、语句级触发器或预计算方案。

触发器里执行复杂查询会直接拖垮写入性能
因为复杂查询(比如带JOIN、子查询、聚合函数的SELECT)在触发器中是同步执行的,且每行变更都跑一遍。一次INSERT 1000行,就等于执行1000次全表扫描或索引范围扫描——事务锁不释放、undo log不清理、binlog堆积,写入延迟立刻从毫秒级跳到秒级。
常见错误现象:SHOW PROCESSLIST 显示大量 Updating 或 Waiting for table metadata lock;INFORMATION_SCHEMA.INNODB_TRX 中 TRX_ROWS_MODIFIED 瞬间飙高;主从延迟突然上涨。
- MySQL 的
EXPLAIN不显示触发器内语句,你看到的“快SQL”可能正被触发器里的SELECT SUM() FROM big_table WHERE user_id = NEW.user_id卡死 - PostgreSQL 行级触发器调用函数时,
select_type = DEPENDENT SUBQUERY出现一次,就是1000行更新=1000次子查询 - 哪怕只是
SELECT COUNT(*) > 0判断存在性,也比EXISTS多扫几万行——触发器里没有“只查到一个就停”的优化意识
复杂查询在触发器中极易引发死锁和隐式锁升级
触发器运行在主事务上下文中,所有操作共享同一事务ID和锁资源。一旦触发器内查询涉及其他表,尤其是那些也被高频更新的汇总表或状态表,就很容易形成跨表锁等待闭环。
典型死锁链:INSERT INTO orders → 触发器执行 UPDATE user_stats SET total_orders = total_orders + 1 WHERE user_id = NEW.user_id → 同时另一个事务正在 UPDATE user_stats → 双方互相等待对方释放行锁。
- MySQL 不允许在触发器中显式控制事务边界,
START TRANSACTION直接报错ERROR 1356 -
SELECT ... FOR UPDATE在触发器里会把读锁升级为写锁,且无法回滚部分失败 - 高并发下多个触发器争抢同一张统计表(如
daily_summary)主键,Deadlock found when trying to get lock成高频错误
调试和监控完全失效,问题藏得深还难复现
触发器逻辑不记录在慢查询日志里,也不出现在 EXPLAIN 或应用层 APM 调用链中。出问题时你只能靠猜:到底是主SQL慢,还是触发器里某条没索引的 SELECT 拖住了整个事务?
-
slow_query_log只记INSERT INTO orders,不标触发器名,更不打点其中任意一条内部语句 - 错误堆栈像
Error Code: 1422. Explicit or implicit commit is not allowed,根本看不出哪行触发器代码调了存储过程 - 想查执行耗时?得手动翻
performance_schema.events_statements_history_long,但SQL_TEXT默认截断,长逻辑要拼凑 - 开发环境单表单行测通,一上生产批量导入就卡死——因为触发器逐行执行的特性,在测试数据量下完全不暴露问题
替代方案不是“怎么写好”,而是“根本别在这里写”
真正需要校验或聚合的场景,应把逻辑移出触发器:用应用层预查、冗余字段、Generated Column、Binlog 解析(如 Canal)、定时任务 + 存储过程,或者 PostgreSQL 的语句级触发器 + 过渡表。
- 必须实时?改用
INSERT ... ON DUPLICATE KEY UPDATE或MERGE,避开子查询 - 要跨表校验?把
user_status冗余到订单表,而不是每次JOIN users - 真要聚合?加预计算字段,例如
ALTER TABLE users ADD COLUMN order_count INT DEFAULT 0,触发器只做UPDATE ... SET order_count = order_count + 1 - PostgreSQL 用户注意:
FOR EACH STATEMENT+NEW TABLE可把1000次调用压成1次,但得重写函数逻辑,不能依赖NEW行变量
最常被忽略的一点:触发器里哪怕只有一行 SELECT 没走索引,整个写入链路就会卡在那里——不是看代码多不多,而是看执行计划里有没有 type: ALL。

















