← [ BACK_TO_NOTES ]

BodySense 数据库第 5 课:Schema Migration 与 Expand-and-Contract

从 BodySense golang-migrate 与 PostgreSQL baseline 出发学习版本化 schema、data backfill、锁风险、应用兼容窗口与 expand-and-contract。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

BodySense 数据库第 5 课:Schema Migration 与 Expand-and-Contract

[!abstract] 本课目标 Migration 不是“把当前 Model 同步到 DB”,而是从旧持久状态到新持久状态的可审计转换历史。

从零建立概念:为什么数据库 Schema 一定会变化

真实产品不会永远使用第一版表结构。

需求变化会带来:

新增列
删除旧列
修改类型
新增约束
新增索引
拆表 / 合表
数据 backfill
状态模型变化

这叫 Schema Evolution。

难点是:

数据库里已经有真实数据,而且旧版本应用可能还在运行。

所以不能把 migration 理解成“开发环境删库重建”。

Migration 是什么

Database Migration 是:

把数据库从一个已知 schema/data state,按照版本化步骤转换到另一个 state。

典型:

v41
↓ 000042_add_source_version.up.sql
v42

Migration 文件本身是历史。

它回答:

以前长什么样?
怎么变成现在?
部署顺序是什么?
出错停在哪个版本?

而 ORM Model 只回答:

当前 Go 代码希望怎样映射

DDL 与 DML

DDL

Data Definition Language:改变结构。

CREATE TABLE
ALTER TABLE
CREATE INDEX
DROP COLUMN

DML

改变数据。

INSERT
UPDATE
DELETE

生产 migration 经常同时包含:

DDL
+
Data backfill

这也是风险所在:结构变化和大量数据写入可能有不同的锁/时间特征。

Up / Down Migration

常见工具允许:

up
→ 向前升级

down
→ 尝试回退结构

但一定记住:

Down SQL 存在,不等于数据层面一定可逆。

例如:

v2 DROP old_column
↓
old data gone

后来再:

ADD old_column

只能恢复列,不会自动恢复原来的值。

所以 release rollback 与 database rollback 是不同问题。

为什么 AutoMigrate 在生产里风险更高

ORM AutoMigrate 的诱惑:

改 struct
→ 启动应用
→ schema 自动变

问题是它弱化了:

  • 版本化历史;
  • 人工审查 SQL;
  • rollout 顺序;
  • lock 风险;
  • backfill 设计;
  • 新旧版本兼容窗口。

BodySense 当前用 golang-migrate + SQL migration,更适合显式治理生产 schema。

Expand-and-Contract 为什么是线上演进主线

假设旧字段:

name

新设计改成:

display_name

危险做法:

一个 migration 直接 DROP name
ADD display_name NOT NULL
同时部署新代码

因为 deployment 不是原子事件。

真实过程中可能同时存在:

old API pods
new API pods
migration running
background jobs

Expand-and-Contract 把变化拆开:

Expand

先新增 display_name
保持 old code 还能工作

Backfill

历史 rows
name → display_name

大表要考虑:

batch size
lock
WAL volume
replica lag
query load

Switch

新代码开始读/写新字段,必要时双写/兼容读。

Contract

确认旧版本完全退出、数据迁移验证后再移除旧字段。

DDL 也有 Lock 语义

例如:

ALTER TABLE
CREATE INDEX
VALIDATE CONSTRAINT

不同操作会拿不同 lock。

所以:

Migration SQL 能在测试库 100ms 跑完,不等于在线大表上就安全。

PostgreSQL 提供 CREATE INDEX CONCURRENTLY 等在线化能力,但它本身有额外限制和失败状态,需要理解后使用。

Constraint 收紧为什么常要分阶段

想把 nullable 列改成 NOT NULL:

如果历史已经有 NULL:

ALTER ... SET NOT NULL
→ validation fails

更可控:

1. add column / compatibility
2. new writes start populating
3. backfill old rows
4. verify no NULL
5. add/validate constraint
6. contract old compatibility

这是“数据事实先满足规则,再让数据库强制规则”。

BodySense Baseline 与未来 Migration 的区别

当前 vNext baseline 注释明确说明:

pre-user phase
→ allowed reset legacy chain
→ capture canonical PG18 baseline

这是项目早期的特殊自由度。

一旦已有真实用户数据:

reset database

就不再是普通可选方案。

因此课程后续按“已有生产数据”的标准训练 migration 思维。

1. Model 与 Migration 是不同事实

Go model
= 当前应用希望看到的 shape

Migration
= 数据库从 version N → N+1 的历史动作

这就是为什么 production 不应仅依赖 ORM AutoMigrate。

2. Schema change 的三类风险

Data risk   → 旧数据不满足新约束
Lock risk   → DDL 获得强锁阻塞线上流量
Compat risk → 新旧应用版本同时运行时 schema 不兼容

3. Expand-and-Contract

一个典型安全流程:

Expand:    先添加兼容的新列/表/索引
Backfill:  填历史数据
Dual/Read: 新代码逐步使用新结构
Contract:  确认旧版本退出后再删旧结构

例如直接给大表新增最终 NOT NULL 且没有 default/backfill 可能失败或造成风险,应根据 PostgreSQL 实际版本与 DDL 行为设计。

4. BodySense 当前锚点

apps/api/internal/database/migrate.go
apps/api/migrations/000001_vnext_baseline.up.sql
apps/api/migrations/checksums.sha256

当前 vNext baseline 是一次 pre-user reset 后的 canonical PostgreSQL 18 schema;未来真实用户数据存在后,migration 的兼容性要求会明显提升。

Prediction

[!question]

  1. 只修改 Go struct,不写 migration,会发生什么?
  2. 应用二进制回滚是否自动等于数据库 schema 回滚?
  3. 为什么“新增 nullable 列 → backfill → 收紧 NOT NULL”通常比一步到位更容易控制?
  4. 新索引一定可以随便在线 CREATE 吗?
  5. migration 成功执行是否足以证明生产发布安全?

[!success]- Answer / 解析 1. 数据库真实 schema 不会自动拥有同样变化,可能在运行期出现列缺失、类型/约束不匹配。

2. 不等于。 Schema/data migration 可能不可逆,新旧应用也可能要求不同 shape;回滚要单独设计兼容窗口。

3. 它把结构扩展、历史数据修复和约束收紧拆开,降低一次 migration 同时承担多种风险。

4. 不一定。 普通 CREATE INDEX 会涉及锁和大量 I/O;生产常需要考虑 CREATE INDEX CONCURRENTLY 及其限制。

5. 不足。 还需验证新旧版本兼容、数据 backfill、锁/延迟、回滚边界和真实读写。

Lab

设计一个 BodySense 迁移:给一个已有大表增加最终必填的 source_version。写出 Expand → Backfill → Contract 三阶段,以及每阶段可观察验证。

Failure Injection

假设 deploy 新 API 后 migration 尚未完成,或 migration 完成但旧 API 仍有实例存活。分别推导失败模式。

L4 Gate

能审查一条真实 migration,并回答:它会拿什么锁、已有数据如何变化、失败能否重试、新旧代码是否兼容。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本