2、Schema版本控制基础:版本号规范(SemVer)、变更日志(Changelog)管理、基线(Baseline)概念
版本控制这事儿,说白了就是给Schema打上标签。没有版本号,你根本不知道线上跑的是哪一套表结构。我见过不少团队,数据库脚本全扔Git里,但版本号乱得像天书——有人用日期,有人用递增数字,还有人直接写"v1-final-真的不改了.sql"。嗯,这种混乱迟早要出问题。
这一章我们聊三个基础概念:版本号怎么定、变更日志怎么写、基线是什么。搞懂这些,后面的演进和迁移才有根基。
2.1 版本号规范:为什么选SemVer
版本号不是随便写的数字。我个人习惯用语义化版本控制(SemVer),格式是 MAJOR.MINOR.PATCH。比如 2.1.3,每个位置都有明确含义:
| 位置 | 名称 | 变更场景 | 示例 |
|---|---|---|---|
| MAJOR | 主版本号 | 不兼容的Schema变更(如删字段、改类型) | 1.0.0 → 2.0.0 |
| MINOR | 次版本号 | 向下兼容的新增(如加字段、加索引) | 1.0.0 → 1.1.0 |
| PATCH | 修订号 | 向下兼容的修复(如改注释、调整默认值) | 1.0.0 → 1.0.1 |
为什么选SemVer?因为它的语义清晰。你看到 3.0.0,就知道这版Schema有破坏性变更,下游应用必须适配。看到 2.5.0,就知道只是加了新东西,旧代码还能跑。
核心原则:MAJOR版本变更时,必须同步更新所有依赖方的兼容逻辑。我曾经在一个项目中,团队把字段类型从INT改成BIGINT,只改了MINOR版本号,结果下游ETL脚本全部报错——因为JDBC驱动对类型转换的处理不一样。那次事故之后,我们定了一条铁律:任何可能影响数据解析的变更,一律升MAJOR。
实际项目中,我建议在MAJOR.MINOR.PATCH后面再加一个构建元数据,比如 2.1.0+build.20240315。这样既能追溯版本,又能知道发布时间。但注意,构建元数据不参与版本比较——2.1.0+build.1 和 2.1.0+build.2 在语义上是同一个版本。
2.2 变更日志(Changelog)管理
版本号只是标签,变更日志才是说明书。没有Changelog,你看到 v3.2.1 时根本不知道改了啥。我见过最离谱的情况:一个Schema从 v1.0.0 升到 v1.1.0,Changelog里只写了一句"优化性能"——结果实际上是加了一个非空字段,导致线上数据写入失败。
好的Changelog应该按版本组织,每个版本包含以下内容:
- Added:新增的表、字段、索引、约束
- Changed:修改的字段类型、长度、默认值
- Deprecated:标记为废弃但尚未删除的字段
- Removed:删除的表或字段
- Fixed:修复的Schema问题(如索引重复、约束错误)
- Security:安全相关的变更(如加密字段、脱敏规则)
我习惯用Markdown写Changelog,格式如下:
# Changelog
## [2.1.0] - 2024-03-15
### Added
- `users` 表新增 `phone_verified` 字段 (BOOLEAN, DEFAULT FALSE)
- 新增 `audit_log` 表,记录数据变更历史
### Changed
- `orders` 表的 `amount` 字段精度从 DECIMAL(10,2) 改为 DECIMAL(12,2)
- `products` 表的 `status` 字段默认值从 'active' 改为 'pending'
### Deprecated
- `users` 表的 `nickname` 字段标记为废弃,计划在 v3.0.0 移除
### Fixed
- 修复 `inventory` 表 `warehouse_id` 索引重复的问题
## [2.0.0] - 2024-02-01
### Removed
- 移除 `legacy_categories` 表
- 移除 `users` 表的 `old_password_hash` 字段
### Security
- `users` 表的 `email` 字段启用静态脱敏
小技巧:Changelog最好和Schema脚本放在同一个仓库里,每次提交都更新。我见过有人把Changelog单独放一个Wiki页面,结果版本升上去了,Wiki忘了更新——等于白写。
写Changelog时要注意粒度。别把"修改了三个字段的注释"这种鸡毛蒜皮的事也写进去,但也不能漏掉关键变更。我的经验是:只要变更会影响下游应用的行为,就必须记录。比如改字段注释不影响运行,但改字段长度就可能影响数据截断——后者必须写。
2.3 基线(Baseline)概念
基线是什么?说白了就是一个已知正确、可重复的Schema快照。你想想看,如果每次部署都要从头执行所有历史脚本,那得跑多久?而且中间任何一个脚本出错,整个流程就断了。
基线的核心作用有两个:
- 快速重建:从基线直接恢复到某个历史版本,不用重跑所有脚本
- 变更追溯:基线记录了某个时间点的完整Schema状态,方便对比差异
我参与过一个金融项目,数据库有300多张表,Schema脚本超过2000个。每次新环境部署,光跑脚本就要4个小时。后来我们引入了基线机制:每发布一个MAJOR版本,就导出一份完整的DDL作为基线。新环境部署时,先导入基线DDL,再按顺序执行基线之后的增量脚本。部署时间从4小时降到了20分钟。
注意:基线不是随便导出的。它必须经过验证——基线DDL要在空数据库上执行成功,并且和当前生产环境的Schema完全一致。我曾经遇到过一个坑:基线DDL里漏了一个外键约束,结果部署到预发环境后,数据写入时才发现约束不匹配。从那以后,我们每次生成基线都会做一次全量对比校验。
基线的命名规范也很重要。我建议用 baseline_{MAJOR}_{MINOR}_{PATCH}.sql 的格式,比如 baseline_2_1_0.sql。这样一眼就能看出基线对应的版本。同时,基线文件要放在单独的目录里,和增量脚本分开管理:
schema/
├── baselines/
│ ├── baseline_1_0_0.sql
│ ├── baseline_2_0_0.sql
│ └── baseline_3_0_0.sql
└── migrations/
├── v2.0.1/
│ ├── 001_add_index.sql
│ └── 002_alter_column.sql
├── v2.1.0/
│ └── 001_add_table.sql
└── v3.0.0/
├── 001_drop_table.sql
└── 002_rename_column.sql
基线的生成频率取决于你的发布节奏。我个人建议:每发布一个MAJOR版本,或者每积累50个增量脚本,就生成一次新基线。这样既不会太频繁,也不会让增量脚本堆积过多。
2.4 三者如何协同工作
版本号、变更日志、基线,这三者不是孤立的。它们构成了Schema版本管理的三角:
- 版本号是标识,告诉别人当前Schema处于哪个状态
- 变更日志是记录,告诉你从上一个版本到当前版本发生了什么
- 基线是快照,让你能快速回到或重建某个版本
举个例子:假设你从 v2.0.0 升级到 v2.1.0。流程是这样的:
- 查看Changelog,了解
v2.1.0新增了哪些变更 - 如果当前环境是
v2.0.0,直接执行增量脚本 - 如果当前环境是全新的,先导入
baseline_2_0_0.sql,再执行v2.1.0的增量脚本 - 升级完成后,更新版本号标记
这个流程看起来简单,但实际项目中经常出问题。我记得有一次,团队在 v2.0.0 和 v2.1.0 之间漏了一个热修复版本 v2.0.1,结果直接升 v2.1.0 时,增量脚本依赖了 v2.0.1 中新增的字段,导致执行失败。从那以后,我们强制要求:增量脚本必须按版本顺序执行,不能跳版本。
总结一下:版本号是门牌号,变更日志是地图,基线是地基。三者缺一不可。没有版本号,你找不到门;没有变更日志,你不知道路怎么走;没有基线,你连站的地方都没有。