应选before update以捕获可靠历史快照:它在更新前可安全读取old.字段,而after中old虽可用但受级联/外键影响可能失真;快照表须含original_id、version等字段并显式插入,避免用old.通配。

触发器里用 AFTER UPDATE 还是 BEFORE UPDATE?
历史快照的核心是“保留旧值”,所以必须在更新发生前捕获原数据。BEFORE UPDATE 触发器能访问 OLD.<em></em>,而 AFTER UPDATE 中 OLD. 仍可用,但若主表有级联或外键约束,可能已不可靠。稳妥做法是统一用 BEFORE UPDATE。
-
BEFORE UPDATE中可安全读取OLD.id、OLD.name等字段 - 不要在
BEFORE UPDATE中修改NEW.*来“绕过”快照逻辑,否则可能干扰业务语义 - 若需记录操作人或时间戳,建议从应用层传入(如
NEW.updated_by),避免在触发器里调用NOW()或USER()——后者返回的是 MySQL 连接用户,不是业务用户
历史表结构怎么设计才不踩坑?
快照表不是主表的简单复制。必须加至少三个字段:id(自增主键)、original_id(对应主表的主键)、version(版本序号或时间戳)。否则无法区分同一记录的多次变更。
-
original_id必须建索引,否则按主键查历史会变慢 - 不要复用主表的
id作为快照主键,否则删除+重插会导致历史丢失 - 推荐用
version字段存UNIX_TIMESTAMP(OLD.updated_at)或递增整数,别用NOW()——并发更新时可能撞时间 - 如果主表有
JSON字段,快照表也应定义为JSON类型,避免隐式转字符串导致格式损坏
INSERT INTO history_table SELECT ... 容易出什么错?
直接 INSERT INTO history_table SELECT OLD.* FROM DUAL 看似简洁,但隐患多:
- 主表字段增减后,触发器不会自动同步,报错
Column count doesn't match value count -
OLD.*不包含计算列、虚拟列,漏数据 - 若历史表有
NOT NULL字段(如created_at),而没显式赋值,会插入失败
更可靠的做法是显式列出字段:
INSERT INTO user_history (original_id, name, email, status, version, created_at) VALUES (OLD.id, OLD.name, OLD.email, OLD.status, UNIX_TIMESTAMP(), NOW());
- 所有字段名都写死,改表结构时触发器必须同步改,这是必要成本
-
created_at用NOW()记录快照入库时间,和业务的updated_at分开 - 如果主表有
TEXT或大字段,确认历史表对应字段类型一致(比如别把MEDIUMTEXT写成VARCHAR(255))
DELETE 和 INSERT 怎么处理才算完整快照?
只做 UPDATE 快照不够。真实场景中,软删除(status = 'deleted')应记快照;硬删除需额外记录删除事实;新插入记录一般不记快照,除非业务要求追溯“首次创建”。
- 硬删除用
BEFORE DELETE触发器,INSERT 到历史表,并设op_type = 'DELETE'字段标识 - 软删除建议在
BEFORE UPDATE里判断:IF OLD.status != 'deleted' AND NEW.status = 'deleted' THEN ... - 新增记录(
BEFORE INSERT)通常跳过快照,但如果要审计“谁在什么时候创建了这条记录”,可加op_type = 'INSERT'字段并记录
快照的完整性取决于你是否把“数据生命周期”的每个关键节点都覆盖到了。最容易被忽略的是:软删除状态变更不等于普通更新,它需要独立识别逻辑,而不是依赖字段值差异。











