绝大多数情况下应优先写在应用层;仅当逻辑必须覆盖所有写入路径、需事务内原子回滚且不依赖外部服务时,才可用触发器,如审计字段强制填充、跨表约束校验、状态流转强校验及不可绕过操作日志。

绝大多数情况下,业务逻辑该写在应用层;触发器只在极少数必须由数据库原子性兜底的场景里才值得用。
哪些操作必须用触发器
触发器不是“备选方案”,而是“最后一道防线”——仅当逻辑必须对所有写入路径生效、且无法被绕过时才考虑:
- 审计字段强制填充:比如
created_at、updated_by不允许应用传入,只能由数据库生成 - 跨表约束校验:如订单插入时检查“客户余额 ≥ 订单金额”,
CHECK约束做不到跨表,而应用层可能被存储过程或 ETL 绕过 - 状态流转强校验:如订单只能从
'pending'→'confirmed',禁止跳转到'cancelled',且该规则必须对 DBA 手动 SQL、ETL 工具、所有应用一视同仁 - 不可绕过的操作日志:每次删敏感表记录,都必须同步写入带
OLD值的audit_log表,且日志写入失败时整个删除语句必须回滚
哪些看似合理但实际不该用触发器
这些场景表面看“自动省事”,实则埋下一致性、可观测性、可维护性三重隐患:
- 在
AFTER INSERT里调用 HTTP 接口发通知——触发器不支持外部调用,MySQL 直接报错,PostgreSQL/SQL Server 也无标准机制 - 用触发器同步冗余统计字段(如把订单明细和写入
order_summary.total)——批量导入时性能陡降,且mysqldump默认不导出触发器,恢复后逻辑静默失效 - 在
BEFORE UPDATE里悄悄改status字段,而应用层还按旧值做判断——竞态下读已提交(RC)隔离级别无法保证应用看到最新值,极易逻辑错乱 - 两个触发器互相更新对方表——MySQL 报
ERROR 1422或Maximum stored procedure nesting level exceeded,堆栈里根本看不出是哪个触发器引发的
怎么判断当前需求该放哪层
直接问三个问题,答案全为“是”才考虑触发器:
- 该规则是否必须覆盖所有写入路径(包括 DBA
UPDATE、ETLBULK INSERT、跨库 CDC 同步)? - 该操作是否必须与主 DML 在同一事务中完成,且失败时能自动回滚(比如校验失败要中断整个
INSERT)? - 该逻辑是否完全不依赖外部服务、不产生不可回滚副作用(如发消息、写文件、调 API)?
只要有一个答“否”,就该放在应用层。例如发邮件、写 Kafka、更新 Redis 缓存、调用风控接口——这些都必须由应用控制重试、降级、补偿,触发器既做不到,也不该做。
混合使用时最易忽略的细节
触发器和应用层共存时,真正的坑不在“能不能用”,而在“谁先看到数据”:
- 应用层读取某行后,在事务内做计算,同时触发器在
BEFORE UPDATE中修改了该行另一字段——应用代码仍基于旧值执行,结果错误。规避方式只有SELECT ... FOR UPDATE重读。 - 触发器里做了
SELECT ... FROM big_table WHERE user_id = NEW.user_id却没加锁——并发下会读到脏数据或引发死锁,错误信息只显示Lock wait timeout exceeded,查不到触发器本身。 - MySQL 的
NEW.id在BEFORE INSERT中为NULL,想写日志却拿不到主键;必须用AFTER INSERT,但此时不能再改原表字段。
这些不是边缘情况,而是日常上线后半夜报警的真实来源。触发器越“隐形”,出问题时定位成本越高——它没有调用栈,不出错时不露面,一出错就让整条 SQL 失败,连日志都得翻全库定义才能确认是否被它影响。

















