触发器里重复查同一张表性能会崩,因为每次dml都重新执行select、无缓存复用、易全表扫描;若字段无索引,exists或count(*)成o(n)操作,吞吐骤降;多触发器或同触发器内多次查询相同条件更恶化问题。

触发器里重复查同一张表,为什么性能会崩
因为每次 INSERT/UPDATE 都会重新执行 SELECT,没缓存、不复用、还可能全表扫。更糟的是,如果查的字段没索引,SELECT EXISTS 或 SELECT COUNT(*) 会变成 O(n) 操作,写入吞吐直接掉一半。
常见错误是:在多个触发器(比如 BEFORE INSERT 和 BEFORE UPDATE)里各自写一遍几乎相同的查表逻辑;或者同一个触发器里对同一条件查两次(一次判断、一次赋值)。
- MySQL 不支持触发器内变量跨语句共享,
@var在下一条语句就失效,没法“查一次、用多次” - PostgreSQL 的触发器函数里局部变量只在函数生命周期内有效,但若触发器被多次调用(如批量 INSERT),每次都是新上下文
- 别指望用
SELECT … INTO后再IF判断能省事——它只是把查和判拆开,没减少查询次数
怎么让触发器只查一次关键数据
核心思路是:把“查”动作压到最必要的一次,且确保后续逻辑复用结果;同时避免在触发器里做本可由约束或应用层承担的检查。
- 用
SELECT … INTO @var查一次,后面所有IF @var IS NOT NULL都复用这个变量(MySQL 会话级变量在单条语句内有效) - 把多条件校验合并成单个
EXISTS或带CASE的SELECT,而不是分三次SELECT查不同字段 - 如果要查关联表状态(比如用户余额、订单状态),优先确认该字段有索引;否则加锁(
FOR UPDATE)反而更慢,不如去掉触发器、改用应用层+分布式锁 - 禁止在触发器里调用存储过程再查表——这等于嵌套一次查询,还增加解析开销
哪些查询根本不用放进触发器
很多你以为“必须查”的逻辑,其实数据库原生机制就能拦住,放触发器纯属徒增延迟和风险。
-
UNIQUE约束本身就能防重复,BEFORE INSERT里再SELECT EXISTS是冗余校验,且并发下照样漏(两个事务同时查到“不存在”,然后都插入) - 主键或自增字段冲突,约束在校验阶段就报错,触发器根本没机会运行——所以别在触发器里试图改
NEW.id来“修复”重复主键 - 时间窗口类判断(如“当天只能提交一条”),应建函数索引
UNIQUE INDEX ON t ((user_id, DATE(created_at))),而不是每次 INSERT 都SELECT COUNT(*) WHERE DATE(created_at) = CURDATE() - 格式校验(长度、正则、非空)用
IF NEW.field IS NULL OR LENGTH(NEW.field) 就够,不需要查表
触发器查表时最容易被忽略的锁代价
你以为只是“读一下”,但 MySQL 默认的快照读(MVCC)在某些场景下会悄悄升级为锁读,尤其当触发器里用了 SELECT FOR UPDATE 或没走索引的 SELECT。
-
SELECT ... FOR UPDATE在触发器里会持行锁,直到父事务结束;高并发下极易引发锁等待甚至死锁 - 没索引的
WHERE条件会让SELECT升级为表锁(MySQL),整个表写入阻塞 - 即使只是
SELECT ... LOCK IN SHARE MODE,也会和其他事务的排他锁冲突,拖慢整体事务响应 - 真正安全的读是无锁快照读,但前提是接受“可能读到旧值”——如果业务不能容忍,说明这个校验本就不该放在触发器里
触发器不是万能胶,它越想包揽“实时性 + 安全性 + 复杂逻辑”,就越容易在锁、性能、一致性之间反复失衡。多数时候,删掉触发器里的查表语句,换成唯一索引 + 应用层重试,反而更快更稳。










