必须用AFTER UPDATE记录字段修改历史,因其确保变更已提交,能安全、一致地读取OLD和NEW的完整状态;BEFORE因事务未落地、快照不可靠而禁用。

触发器该用 BEFORE 还是 AFTER
记录字段修改历史,必须用 AFTER UPDATE,不能用 BEFORE UPDATE。因为 BEFORE 阶段新值还没真正写入行,NEW 虽然可读,但此时原记录可能尚未提交,且无法可靠获取事务级一致性快照;而 AFTER 确保变更已落地,能安全读取 OLD 和 NEW 的完整状态。
常见错误是误用 BEFORE 并试图在日志表中查刚更新的主键——结果查不到或查到旧值。
-
AFTER UPDATE是唯一能保证OLD/NEW与最终持久化数据一致的时机 - 若需阻止非法修改(比如禁止将 status 从 'done' 改回 'pending'),才用
BEFORE+RAISE或赋值拦截 - MySQL 不支持
BEFORE中对本表 INSERT/UPDATE(会报Can't update table in stored function)
如何只记录实际变化的字段
直接对比 OLD.col 和 NEW.col,避免无意义日志。NULL 值比较要显式用 IS NOT DISTINCT FROM(PostgreSQL)或 (OLD.col = NEW.col) OR (OLD.col IS NULL AND NEW.col IS NULL)(MySQL / SQLite)。
例如想只记录 email 和 status 的变更:
IF OLD.email != NEW.email OR (OLD.email IS NULL) != (NEW.email IS NULL) THEN INSERT INTO user_history (user_id, field, old_value, new_value, updated_at) VALUES (NEW.id, 'email', OLD.email, NEW.email, NOW()); END IF;
- 字符串比较注意大小写和空格:MySQL 默认不区分大小写,建议用
BINARY或调整 collation - JSON 字段慎用
=比较——不同顺序的 key 可能判为不等,应先标准化再比 - 时间字段用
UNIX_TIMESTAMP或CAST统一类型,避免隐式转换导致误判
触发器里写 INSERT 性能很慢怎么办
单条 UPDATE 触发 INSERT 日志,本身开销不大;但高并发下容易成为瓶颈,尤其日志表没建好索引或用了 MyISAM 引擎。
- 日志表务必用
ENGINE=InnoDB,并给updated_at和外键字段(如user_id)加复合索引 - 避免在触发器里调用函数(如
UUID()、NOW()多次)——每行触发都执行,浪费 CPU - MySQL 8.0+ 可考虑用
INSERT ... ON DUPLICATE KEY UPDATE合并短时间内的重复变更,但需业务允许“去重” - 真正扛不住时,改用应用层异步写日志(如发 Kafka 消息),触发器只做轻量标记
PostgreSQL vs MySQL 的语法关键差异
PostgreSQL 触发器函数必须返回 NEW(AFTER 可返回 NULL),MySQL 则无需 RETURN;且 PG 的 NEW/OLD 是记录类型,引用字段必须带表别名或用 NEW.field 显式写法。
- MySQL:
CREATE TRIGGER tr_user_upd AFTER UPDATE ON users FOR EACH ROW INSERT INTO user_history ... - PostgreSQL:
CREATE OR REPLACE FUNCTION log_user_change() RETURNS TRIGGER AS $$ BEGIN INSERT INTO user_history ...; RETURN NEW; END; $$ LANGUAGE plpgsql; - PG 中
NEW.*不能直接展开插入,必须逐字段写NEW.id, NEW.email等 - MySQL 5.7+ 支持多语句触发器,但每个
INSERT仍算一次独立语句,注意 max_allowed_packet 限制
跨数据库移植触发器前,先确认目标库是否支持行级触发器(SQLite 仅支持 BEFORE,且不能修改本表)。

















