触发器必须建在目标业务表(如orders)而非历史表(如orders_history);insert/update/delete需分三个独立触发器;时间戳用now()或current_timestamp,操作人优先用current_user或user();历史表须建op_time与source_id联合索引;insert务必显式指定字段列表。

触发器该往哪张表上建:必须是目标业务表,不是历史表
想记录 orders 表的变更,触发器就得建在 orders 上,而不是建在你预设的 orders_history 上。MySQL 和 PostgreSQL 都不支持“对历史表建触发器来监听业务表”的写法——触发器的宿主只能是它要响应的那张表。
常见错误是先建好 orders_history,再试图给它加 BEFORE INSERT 去捕获源头变更,结果完全没反应。记住:触发器的 ON 后面,永远是你真正 UPDATE/DELETE/INSERT 的那张表。
INSERT/UPDATE/DELETE 三类操作要分开写触发器
一个触发器不能同时响应 INSERT 和 UPDATE(MySQL 5.7+、PostgreSQL 支持单触发器多事件,但逻辑混在一起极易出错)。实际维护中,强烈建议拆成三个独立触发器:
-
orders_after_insert:只存新插入的完整快照,op_type = 'I' -
orders_after_update:用OLD.*记旧值,NEW.*记新值,可选只存变化字段或全量 -
orders_after_delete:只能用OLD.*,此时行已不存在,必须立刻落库
别图省事合并逻辑——比如在 UPDATE 触发器里判断 IF NEW.status != OLD.status 再写日志,这会漏掉其他字段变更;更糟的是在 DELETE 里尝试查关联表补数据,可能因外键约束或事务隔离直接失败。
时间戳和操作人字段不能依赖应用层传入
触发器内必须用数据库原生时间函数填 updated_at 或 op_time,例如 NOW()(MySQL)或 CURRENT_TIMESTAMP(PostgreSQL)。如果让应用传 modified_by,得额外加一列接收参数,既增加接口负担,又面临注入风险。
更可靠的做法是:
- PostgreSQL:用
current_user或session_user获取当前连接身份 - MySQL:8.0+ 可用
USER(),但注意它返回'user@host'格式,需SUBSTRING_INDEX(USER(), '@', 1)截取用户名 - 若需精确到应用用户(如工号),应在业务表中预留
last_modified_by字段,由应用每次更新时显式赋值,触发器再将其抄入历史表
性能隐患:大表 UPDATE 触发器可能锁表或拖慢主流程
触发器是同步执行的——UPDATE orders SET amount = 100 WHERE id = 123 这条语句,会等触发器把日志写完才返回成功。如果历史表没建好索引、或磁盘 I/O 慢,主业务就卡住。
关键缓解措施:
- 历史表务必给
op_time和source_id(即原表主键)建联合索引,避免SELECT * FROM orders_history WHERE source_id = 123 ORDER BY op_time DESC LIMIT 1全表扫 - 禁止在触发器里做跨库查询、调用存储过程、发 HTTP 请求——这些不是“原子操作”,且不可回滚
- 高频小更新场景(如计数器字段),考虑改用应用层异步写日志,而非强一致性触发器
最常被忽略的一点:触发器里的 INSERT INTO history_table 如果没指定字段列表(比如写成 INSERT INTO orders_history VALUES (...)),一旦历史表后期加字段或调整顺序,整个触发器就失效报错,且很难定位——务必显式写出所有目标列名。










