最推荐历史表分离法,即通过before update/delete触发器将原记录存入独立历史表,并建user_id+updated_at复合索引;辅以主表version+is_current逻辑版本控制或json字段轻量日志,三者按查询频率、回溯需求与写入成本权衡选用。

用历史表分离法自动存档变更前数据
这是最贴近“数据库结构版本管理”实际需求的做法——不是管MySQL服务版本,而是管业务表结构或关键数据的演化过程。核心是让每次UPDATE/DELETE前,把原记录完整存进历史表,靠触发器驱动,无需改应用代码。
常见错误现象:user_info 表被反复UPDATE,但查不到谁在什么时候改了邮箱;回滚只能靠备份,粒度太粗。
- 必须为历史表单独建索引,比如
user_id+updated_at,否则按时间查历史会慢得无法接受 -
BEFORE UPDATE触发器里用OLD.*插入,不能用NEW.*,否则存的是新值 - 删除操作要用
BEFORE DELETE,否则OLD在触发器执行后就不可用了 - 历史表的
id字段建议设为AUTO_INCREMENT,别复用主表id,避免主键冲突和语义混淆
主表加 version + is_current 字段做逻辑版本控制
适合需要频繁查询最新状态、同时保留历史但不常查全量版本的场景,比如用户资料、产品配置。它把版本信息压进主表,省去JOIN历史表的开销,但写操作变重。
使用场景:前端展示“当前生效的套餐配置”,后台允许回滚到任意历史版本并设为当前。
- 每次更新必须两步:先将旧记录的
is_current设为0,再插入新记录且is_current = 1,顺序不能反,否则出现两个is_current = 1 -
version字段建议用INT UNSIGNED,从1开始递增,不要依赖MAX(version)+1(并发下可能重复) - 查询最新数据时,务必加
WHERE is_current = 1,否则可能读到一堆过期行 - 这个方案对
SELECT * FROM user_info这类无条件查询不友好,容易误读旧数据
用 JSON 字段轻量记录字段级变更日志
当只需要知道“哪个字段变了、变成啥、谁改的”,不需要还原整行快照时,JSON 日志是最省空间、最易扩展的方式。适用于审计、调试、低频回溯。
基于三引擎设计,从微信文章、新闻和博客网页提取干净内容,支持标题作者日期元数据,多格式和批量处理。
性能影响:单次UPDATE多写一个JSON字段,比触发器+INSERT历史表轻量得多;但JSON解析查起来比原生字段慢,不适合高频统计。
- 推荐结构:
{"field": "email", "old": "a@old.com", "new": "a@new.com", "by": "admin", "at": "2026-07-01 14:22:03"} - 不要把整个变更存成大JSON对象,只存差异字段,否则日志膨胀极快
- MySQL 5.7+ 支持
JSON_CONTAINS()、JSON_EXTRACT(),但复杂查询仍建议额外建普通字段辅助,比如changed_by和changed_at - JSON字段不支持全文索引,想按旧值搜索就得靠正则或应用层解析,慎用
注意事务隔离与MVCC对版本可见性的影响
无论用哪种结构存版本,读取时都绕不开InnoDB的MVCC机制。同一个事务里,两次 SELECT 可能看到不同版本的数据,这不是bug,是设计使然。
容易踩的坑:REPEATABLE READ 下事务内第一次读生成Read View,后续读都沿用它——这意味着你刚插入一条新版本记录,在同一事务里 SELECT 可能看不到它,除非显式用 SELECT ... FOR UPDATE 触发当前读。
- 如果业务逻辑依赖“插入即可见”,必须用
READ COMMITTED隔离级别,或强制当前读 - 历史表本身也受MVCC影响,
SELECT * FROM user_info_history查到的只是该事务视角下的快照,不是绝对时间线上的全部变更 - 跨版本对比(比如比较v3和v5)时,别假设它们在同一个事务里可比,要明确指定
AS OF TIMESTAMP或用BINLOG定位精确时间点
实际落地时,选哪种方式不取决于“高级不高级”,而取决于你最常怎么查、多久回溯一次、能不能接受写放大。历史表最稳但重,version字段折中,JSON最灵活但难查。没有银弹,只有权衡。










