触发器中禁止查询被修改的表、避免调用存储过程、时间戳用BEFORE或字段默认值实现、跨系统操作应移至应用层。

触发器里做了 SELECT 查询,插入就卡住
MySQL 触发器执行时会锁住当前行(或更多),如果在 BEFORE INSERT 里写 SELECT ... FROM same_table,极大概率触发表级锁或死锁,尤其在高并发插入时。这不是“慢”,是阻塞。
- 避免在触发器中查正在被修改的表,哪怕只是
SELECT COUNT(*) - 真要统计,改用应用层缓存或维护一个单独的计数表,用
INSERT ... ON DUPLICATE KEY UPDATE更新 - 如果必须关联查询,确保被查表有合适索引,且只查必要字段——
SELECT id FROM user WHERE status = 1 LIMIT 1比SELECT * FROM user安全得多
触发器调用了存储过程,性能雪上加霜
触发器本身执行上下文受限,再嵌套调用存储过程,等于把事务内耗放大:每次插入都多一次过程解析、权限检查、变量初始化。实测常见场景下,调用一次简单存储过程会让单条 INSERT 延迟增加 3–8ms(SSD + 8核机器)。
- 把存储过程里的核心逻辑直接抄进触发器体,删掉
CALL;能少一层调用就少一层 - 如果存储过程被多处复用,优先考虑改用应用层函数,而不是让 MySQL 在每行插入时都重跑一遍
- 确认该存储过程没做事务控制(如
START TRANSACTION),否则会和外层插入事务冲突
用触发器实现“自动更新时间戳”,但写了 AFTER UPDATE
AFTER UPDATE 触发器不能修改当前行数据(会报错 Can't update table 't' in stored function/trigger),很多人误以为它适合改时间戳,结果只能退回到 BEFORE UPDATE,又忘了排除无意义更新(比如 UPDATE t SET name='a' WHERE id=1 却没改内容)。
- 时间戳类逻辑一律用
BEFORE INSERT/BEFORE UPDATE - 在
BEFORE UPDATE中加判断:IF OLD.updated_at != NEW.updated_at THEN SET NEW.updated_at = NOW(); END IF;,避免无效赋值 - 更稳妥的做法:把
updated_at设为TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,交给 MySQL 自动管理,不写触发器
触发器逻辑本可用应用层处理,却硬塞进数据库
比如“插入订单后,给用户积分”,这类跨系统、带业务规则、可能失败需重试的操作,放在触发器里只会让问题更隐蔽:错误日志在 MySQL error log 里难排查,无法统一监控,失败后整个插入事务回滚,连订单都写不进去。
- 触发器只做原子性强、副作用小、不依赖外部服务的事:比如生成 UUID、校验格式、补默认值
- 涉及消息通知、积分、库存扣减等,必须由应用发起异步任务,失败可重试、可告警、可补偿
- 如果已有大量触发器逻辑,迁移前先用
SHOW TRIGGERS LIKE 'table_name'拿出清单,逐条评估是否真需要数据库层执行
真正难的不是写触发器,是判断哪一行逻辑不该出现在触发器里——尤其是当别人写的触发器开始拖慢你的插入速度时,第一反应不该是优化它,而是问一句:它非得在这儿吗?


















