高并发下触发器变慢主因是行级同步执行中嵌入join、聚合、子查询等重操作,导致单条写入延迟激增;应改用预计算、异步处理或集合操作,并确保索引覆盖所有查询条件。

高并发下触发器执行变慢,八成是因为在 AFTER INSERT 或 BEFORE UPDATE 里写了带 JOIN、SUM()、子查询的 SELECT,或者反复调用函数查大表——这些操作不是“慢一点”,而是会让单条写入延迟从 0.5ms 拉到 20ms+,1000 QPS 就直接卡住。
触发器里为什么不能写 JOIN 或子查询
触发器是行级同步执行的,每插入一行就跑一次完整逻辑。比如订单表加了个 AFTER INSERT 触发器,里面写:SELECT SUM(amount) FROM order WHERE user_id = NEW.user_id,那每下一单就全表扫一遍用户历史订单。数据过万后,这个 SUM 就变成磁盘 I/O 瓶颈。
常见错误现象:SHOW PROCESSLIST 看到大量线程卡在 Sending data 或 Updating;EXPLAIN ANALYZE 显示 Seq Scan 行数超 10 万;慢日志里 Rows_examined 动辄几十万。
- 必须实时聚合?改用应用层预计算字段(如用户表加
total_spent),INSERT 时只做NEW.total_spent := OLD.total_spent + NEW.amount - 非强一致性场景?把聚合逻辑拆出去,用定时任务或物化视图增量刷新
- 真要查关联表?只允许通过主键或唯一索引单点查询,且加
LIMIT 1,禁用IN (SELECT ...)和NOT IN(NULL 会导致全表扫描)
NEW/OLD 引用不当引发隐式开销
看似安全的 NEW.status 在配合函数时会反复解析:比如 to_timestamp(NEW.ts_str) 或 jsonb_extract_path_text(NEW.payload, 'user', 'id'),每次调用都触发字符串解析+JSON 解构,没缓存、不复用。
MySQL 的 BEFORE INSERT 中甚至不能用 NEW.id 做子查询——自增 ID 还没生成,查出来是 NULL;而 AFTER INSERT 又没法再改 NEW 值,形成设计死锁。
- 先赋值给局部变量:
DECLARE v_user_id INT := NEW.user_id;(PostgreSQL)或在 MySQL 里用SET @uid := NEW.user_id; - 避免在条件判断里重复取同一个字段,尤其别在
IF ... THEN ... ELSE块里各写一遍NEW.xxx - JSON 字段先判类型:
IF json_type(NEW.data) = 'OBJECT' THEN ...,防止对 null 或 string 调用json_extract报错
批量操作让触发器开销翻 N 倍
INSERT INTO t VALUES (), (), () 是 3 行插入,触发器执行 3 次;UPDATE t SET x=1 WHERE id IN (1,2,3,4,5) 是 5 行更新,触发器也跑 5 次。10 万行批量更新 = 触发器逻辑执行 10 万次,哪怕每次只花 1ms,总耗时也 100 秒。
典型踩坑:日志类触发器为每条记录都 INSERT INTO audit_log,audit_log 表没分区、没索引,写入直接锁表。
- 能用集合操作就别依赖触发器:比如用
UPDATE t1 JOIN t2 ON ... SET t1.x = t2.y一次性更新,而不是靠触发器逐行联动 - 高频写入场景下,临时禁用触发器:
ALTER TABLE t DISABLE TRIGGER tr_name;(PostgreSQL)或 MySQL 用SET SQL_LOG_BIN = 0配合手动 binlog 补偿 - 审计类逻辑改用 WAL 日志解析(Canal / Debezium)或
pg_notify()发事件,由外部 worker 异步消费
索引缺失让点查变全表扫描
触发器里一句 SELECT COUNT(*) FROM log_table WHERE status = NEW.status AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR),看着只查最近一小时,但如果 log_table 没建 (status, created_at) 复合索引,就会全表扫描——而这个表可能有千万行。
更糟的是,这种查询在事务中执行,会持锁等待,其他写入被堵住,Innodb_row_lock_waits 指标飙升。
- 所有触发器内
SELECT的WHERE条件字段,必须出现在复合索引最左前缀,且类型完全一致(INT对BIGINT会强制转换,索引失效) - 时间范围查询务必配前缀索引或按天/月分区,避免
created_at > '2024-01-01'这种无界条件退化 - 用
EXPLAIN显式验证每条触发器内 SQL 是否走索引,别信“应该走了”
真正难的不是写出能跑的触发器,而是判断哪部分该留在数据库里、哪部分必须推出去。一个 BEFORE INSERT 里只做 NEW.updated_at := NOW() 和 NEW.version := OLD.version + 1 是安全的;一旦出现 SELECT、UPDATE other_table、CALL proc_xxx(),就要立刻警觉——这不是优化问题,是架构分层问题。










