before触发器中执行select会延长锁持有时间,after触发器中更新原表易引发递归或死锁;批量dml下二者性能衰减模式不同;触发器体超3行sql即现性能拐点。

BEFORE触发器里做SELECT会延长锁持有时间
BEFORE触发器在主语句执行前运行,能改NEW值,但一旦里面出现SELECT ... FOR UPDATE或普通SELECT查大表,就会提前加锁、拖长事务时间。比如在BEFORE INSERT ON orders里执行SELECT balance FROM users WHERE id = NEW.user_id FOR UPDATE,不仅锁住用户行,还让整个插入卡在事务开始阶段。
实操建议:
- 校验类逻辑(如手机号格式)可放
BEFORE,但禁止任何SELECT——只用正则或字符串函数 - 若必须查关联数据(如检查用户状态),改用覆盖索引的单行查询:
SELECT status FROM users WHERE id = NEW.user_id,且确保(id, status)是联合索引 - 避免在
BEFORE中调用含SELECT的存储函数,MySQL不会缓存结果,每行都重跑
AFTER触发器里更新原表容易引发递归或死锁
AFTER触发器能读NEW.id(含自增ID),但此时主记录已落盘,再对同一张表做UPDATE就危险了。例如AFTER INSERT ON logs里写UPDATE logs SET processed = 1 WHERE id = NEW.id,MySQL 8.0+ 默认会报ERROR 1442 (HY000): Can't update table 'logs' in stored function/trigger,5.7则可能静默失败或死锁。
实操建议:
- 绝对禁止在
AFTER触发器中对被触发表自身执行INSERT/UPDATE/DELETE - 跨表写入优先选
AFTER,但目标表必须有主键+写入字段索引,否则INSERT INTO stats VALUES (NEW.user_id, 1)可能因无索引导致全表扫描 - 需防重复写入时,在
AFTER开头加IF NOT EXISTS (SELECT 1 FROM summary WHERE user_id = NEW.user_id),别用INSERT IGNORE——它仍会尝试插入并产生间隙锁
批量DML下BEFORE和AFTER的性能衰减模式不同
执行INSERT INTO t VALUES (1),(2),(3)时,BEFORE INSERT和AFTER INSERT都会各触发3次,但耗时分布不同:BEFORE的开销叠加在主语句解析+锁申请阶段,AFTER则叠加在写入完成后的日志刷盘阶段。压测显示:含INSERT INTO log_table的AFTER触发器会让批量插入延迟翻倍;而同样逻辑放在BEFORE里,还会额外增加主表行锁等待。
实操建议:
- 批量导入(
LOAD DATA INFILE或INSERT ... SELECT)前,用ALTER TABLE t DISABLE TRIGGER tr_name临时禁用(MySQL 8.0.19+支持) - 日志类操作统一用
AFTER,但目标日志表引擎建议设为ENGINE=BLACKHOLE(仅测试)或ENGINE=ROCKSDB(高写场景),避开InnoDB事务开销 - 不要依赖
BEFORE来“预计算”字段值后再批量插入——生成列(GENERATED ALWAYS AS)零运行时成本,更稳
触发器体超过3行SQL就该警惕性能拐点
MySQL不优化触发器内冗余逻辑。比如BEFORE UPDATE里先SET NEW.updated_at = NOW(),又SET NEW.updated_at = UTC_TIMESTAMP(),第二句白跑但CPU照花。实测表明:有效SQL超3行后,单条DML平均耗时上升非线性——4行比3行慢37%,6行直接翻倍。
实操建议:
- 把
IF/ELSE分支内共用的赋值提到最外层,减少重复计算 - 禁用
UUID()、RAND()等非确定函数——它们不仅慢,还会破坏主从一致性 - 真需要复杂逻辑时,宁可用应用层事务包裹主表写+异步消息,也别硬塞进触发器
BEFORE或AFTER这两个词,而是你往里面塞了什么。哪怕时机选对,一行SELECT COUNT(*) FROM big_table也足以让QPS崩掉。











