触发器必然拖慢insert,因其在事务内同步逐行执行且必须全部完成才能提交;哪怕仅一行select count(*)也会使单条插入从0.3ms升至12ms,批量插入时开销呈乘性放大。

为什么触发器会让 INSERT 变慢是必然的,不是“可能”
MySQL 触发器在 INSERT 语句的同一事务中同步执行,且对每一行都逐行调用。这意味着:主 SQL 还没返回,触发器里的逻辑必须全部跑完、提交、刷盘——哪怕只多加一行 SELECT COUNT(*) FROM log_table WHERE status = NEW.status,也会把单条插入从 0.3ms 拉到 12ms。批量插入 1000 行时,这个开销不是累加,而是乘性放大。
哪些操作必须从触发器里删掉
以下行为在触发器中出现一次,就足以让 INSERT 响应时间不可控:
-
SELECT查询其他表(尤其是没索引的WHERE条件),会引发额外锁等待和全表扫描 -
UPDATE或INSERT INTO ... SELECT写入其他表,直接延长事务持有锁的时间 - 调用自定义函数(如
GET_USER_RANK()),每次调用都可能触发隐式子事务 - 使用
NOW()、UUID()、RAND()等非确定性函数,不仅拖慢执行,还破坏主从一致性 - 在
AFTER INSERT中反向UPDATE同一表的汇总行(如更新order_summary),极易和并发事务形成死锁
真正轻量的 BEFORE 触发器长什么样
能留在数据库侧的逻辑,必须满足「无查询、无跨表、无函数、无分支」四不原则。典型安全写法只有这几类:
- 填充默认时间:
SET NEW.created_at = NOW()(仅限binlog_format = ROW环境) - 字段格式清洗:
SET NEW.phone = REPLACE(NEW.phone, ' ', ''),前提是长度可控、无正则 - 空值强制校验:
IF NEW.email IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'email required'; END IF; - 简单数值修正:
SET NEW.amount = ROUND(NEW.amount, 2)
所有这些操作都在内存完成,不访问磁盘、不持锁、不生成新事务上下文。
想保留业务逻辑?那就别用触发器,改用中转表+异步消费
如果逻辑绕不开(比如要实时更新统计、发通知、写审计日志),正确路径是解耦:
- 触发器只做一件事:
INSERT INTO trigger_queue (id, table_name, event_type, created_at) VALUES (NEW.id, 'orders', 'insert', NOW()) - 中转表
trigger_queue必须极简:仅含必要字段,引擎为InnoDB,禁止除主键外的任何索引 - 外部消费者(Python/Go 常驻进程)按
SELECT ... FOR UPDATE SKIP LOCKED LIMIT 100拉取,处理成功后DELETE或UPDATE status = 'done' - 绝不能用
SELECT + DELETE两阶段操作,否则失败重试会丢数据或重复处理
最常被忽略的一点:中转表的 created_at 字段不是摆设——它让你能监控积压时长,一旦发现延迟超 5 秒,立刻知道消费者挂了或逻辑卡住了。











