触发器必须在毫秒级内完成,否则会拖慢主事务、卡住连接池、放大锁等待;应将http调用、mq发送、远程查询、聚合统计等耗时操作全部移出触发器,改用异步方式(如outbox表、cache_invalidatio表、增量更新)处理,并优先使用for each statement替代for each row,结合变更判断与性能监控确保真实开销可控。

触发器必须在毫秒级内完成,否则会拖慢主事务、卡住连接池、放大锁等待——这不是优化建议,而是 OLTP 场景下的硬性约束。
把耗时操作全移出触发器体
触发器里执行 HTTP 调用、发 MQ、查远程库、聚合统计,等于把应用层逻辑塞进数据库事务里。这些操作天然不可控:网络抖动、下游超时、重试逻辑缺失,都会让 AFTER INSERT 卡住整个订单插入流程。
- 日志写入:改用
INSERT INTO outbox(本地表),字段含event_type、payload、status;后台任务轮询处理 - 缓存更新:触发器只写
cache_invalidation表,由独立服务监听并刷新 Redis - 跨表统计:禁用
UPDATE summary_table SET count = (SELECT COUNT(*) FROM detail)这类子查询,改用增量式维护(如SET count = count + 1)
用 FOR EACH STATEMENT 替代 FOR EACH ROW 处理聚合场景
批量导入 1000 条订单时,FOR EACH ROW 触发器会执行 1000 次;而 FOR EACH STATEMENT 只执行 1 次,且能直接访问整批数据(如 PostgreSQL 的 transition tables,MySQL 8.0+ 的 CTE + INSERT ... SELECT 模拟)。
- 适用场景:
AFTER INSERT ON orders更新客户总消费额、订单状态汇总 - 不适用场景:单行校验(如
BEFORE UPDATE检查价格不能为负)——这类必须用FOR EACH ROW - 注意兼容性:SQL Server 不支持 statement-level trigger;Oracle 需用
COMPOUND TRIGGER模拟
加变更判断,跳过未实际修改的字段
用户只改了邮箱,触发器却去重算所有关联统计、写全量审计日志,纯属浪费资源。关键不是“有没有改”,而是“值是否真变了”。
- MySQL 示例:
IF NEW.email != OLD.email THEN INSERT INTO audit_log (...) VALUES (...); END IF; - PostgreSQL 示例:
IF NEW.status IS DISTINCT FROM OLD.status THEN ... END IF;(IS DISTINCT FROM正确处理 NULL) - 避免陷阱:别用
IF NEW.updated_at = OLD.updated_at判断——应用层可能已主动设了新时间戳,但业务字段根本没变
监控触发器真实开销,别信“它很小”
没人会在上线前测触发器的 P99 延迟。等 DBA 在 performance_schema.events_statements_history_long 里看到 TRIGGER 类型语句占了 40% 的平均耗时,已经晚了。
- MySQL:开启
performance_schema后查events_statements_summary_by_digest,过滤DIGEST_TEXT LIKE '%TRIGGER%' - PostgreSQL:启用
pg_stat_statements,关注total_time / calls,特别留意shared_blks_read高的触发器 - 最易被忽略的一点:触发器调用的函数(比如自定义的
generate_audit_id())如果内部有SELECT ... FOR UPDATE,会悄悄把行锁升级成表锁











