触发器中禁止写select查询,因其会拖慢update响应并引发锁等待;避免after update更新同表以防死锁;批量update时触发器逐行执行导致性能骤降;存储过程需显式事务控制。

触发器里别写SELECT查询
这是拖慢 UPDATE 响应最常见、最隐蔽的坑。触发器执行时,MySQL 会锁住当前行(甚至页),如果里面嵌套一条 SELECT 去查大表或带 JOIN 的表,就等于把锁等待和 I/O 开销直接塞进主事务里。
典型错误现象:UPDATE orders SET status = 'shipped' WHERE id = 123 执行变慢,SHOW PROCESSLIST 显示状态卡在 Updating 或 Sending data;高并发下平均响应时间从 2ms 拉到 50ms+。
- 把查用户等级、统计值、关联配置这类逻辑全移出触发器,由应用层提前查好、通过
SET或VALUES()显式传入 - 如果必须查,确保该
SELECT走索引:用EXPLAIN验证,且索引字段包含NEW或OLD引用的列(如WHERE user_id = NEW.user_id,则user_id必须是索引前缀) - 禁止跨库查询,比如
SELECT * FROM other_db.users——连接开销 + 无法复用当前事务上下文
AFTER UPDATE 更新同表极易死锁
很多人习惯用 AFTER UPDATE 去更新汇总表或计数字段,但 MySQL 在 AFTER 阶段仍持有原始行的 X 锁,此时再 UPDATE 同一表(哪怕不同行),极易和其它事务形成循环等待。
真实场景:AFTER UPDATE ON orders → UPDATE order_summary SET total = total + NEW.amount WHERE date = NEW.created_date。在 MySQL 8.0+ 上,这个操作比 5.7 更容易报 Deadlock found when trying to get lock。
- 优先改用
BEFORE UPDATE做字段修正或默认值填充,避免后续写操作 - 统计类、聚合类逻辑一律剥离:用应用层异步任务定时刷新,或写入消息队列表由后台消费
- 真要保留在数据库侧,改用单条
INSERT ... ON DUPLICATE KEY UPDATE合并写入,减少语句数量
批量UPDATE时触发器被逐行调用
UPDATE t SET status = 2 WHERE id IN (1,2,3,...,1000) 这种语句本身很快,但如果表上有行级触发器,MySQL 会为每一行匹配的结果单独执行一次触发器逻辑——不是执行 1 次,而是 1000 次。
性能衰减不是线性的:100 行可能耗时 10ms,1000 行可能飙到 200ms+,因为每次触发都重复解析、加锁、执行 DML。
- 检查是否真需要行级触发器:如果是全量状态变更(如“全部订单标记为已取消”),直接用应用层控制流程,禁用触发器
- 若必须保留,把触发器内逻辑压到最简——只做
NEW.status := 'canceled'这类赋值,删掉所有UPDATE/INSERT和函数调用 - 用
information_schema.TRIGGERS定期审计:有没有多个触发器叠加在同一事件上?有没有被遗忘的测试用触发器还挂着?
存储过程替代触发器反而更危险
有人把一堆触发器逻辑收进一个 proc_update_order_status 存储过程中,以为更可控,结果事务边界乱了:INSERT INTO audit_log 成功了,UPDATE order_main 却失败,audit_log 留下脏数据。
这不是存储过程不行,而是没显式管理事务和错误处理。
- 存储过程开头必须写
START TRANSACTION,结尾配COMMIT或ROLLBACK - 每条 DML 后检查
ROW_COUNT()和@@ERROR,失败立即ROLLBACK并RESIGNAL -
DEFINER权限要最小化:不要用root@'%',指定低权限账号,且只授予过程内实际用到的表权限
真正难的不是写对语法,是判断哪段逻辑“非得塞进事务里”。多数时候,那个“必须原子性”的边界,比你想象中窄得多。











