数据库迁移脚本管理实践:不要手改生产库
wxk1991 Lv5

数据库迁移脚本管理实践:不要手改生产库

数据库结构变化最怕什么?

不是写错一个字段。

而是:

1
2
3
4
开发环境改了
测试环境忘了
生产环境手工补了
过几天谁也不知道真实结构是什么

代码可以 Git 回滚。

数据库如果靠手工改,很快就会失控。

所以项目只要进入多人协作,就应该认真管理数据库迁移脚本。


一、什么是数据库迁移

数据库迁移就是把数据库结构变化写成可追踪脚本。

比如:

1
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);

或者:

1
2
3
4
5
6
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount NUMERIC(12, 2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

这些脚本按顺序执行,数据库就能从旧版本升级到新版本。

迁移管理的目标是:

1
任何环境都能知道自己执行到了哪一步。

二、不要直接在生产库点点点

很多事故来自“我就临时改一下”。

比如:

1
2
3
在生产库加字段
在测试库忘了加
代码上线后测试没问题,生产出问题

或者:

1
2
3
生产库字段类型被手动改了
迁移脚本没有记录
下次自动迁移直接失败

数据库结构是代码的一部分。

既然代码要走 Git,数据库结构也应该走脚本。

原则很简单:

1
2
可以临时排查
不要临时变更

真要紧急修复,也要事后补迁移脚本,并确认所有环境一致。


三、迁移文件命名要有顺序

迁移脚本最重要的是顺序稳定。

常见命名方式:

1
2
3
20260612195700_create_users_table.sql
20260612195800_add_avatar_url_to_users.sql
20260612195900_create_orders_table.sql

也可以用递增版本号:

1
2
3
001_create_users_table.sql
002_add_avatar_url_to_users.sql
003_create_orders_table.sql

我更喜欢时间戳。

因为多人同时开发时,时间戳更不容易冲突,也更容易看出脚本产生顺序。

目录可以这样放:

1
2
3
4
db/
migrations/
20260612195700_create_users_table.sql
20260612195800_add_avatar_url_to_users.sql

不要把迁移脚本散落在文档、聊天记录和个人电脑里。


四、需要一张 schema_migrations 表

迁移工具通常都会维护一张表,记录哪些脚本执行过。

类似:

1
2
3
4
CREATE TABLE schema_migrations (
version VARCHAR(64) PRIMARY KEY,
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
);

每执行一个迁移文件,就插入一条版本记录。

这样应用启动或部署时,就能判断:

1
2
哪些迁移已经执行
哪些迁移还没执行

不要只靠“我记得执行过”。

人脑不是迁移系统。


五、迁移脚本要尽量可重复检查

有些脚本可以写得更安全。

比如 PostgreSQL 里:

1
2
3
4
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL
);

加索引:

1
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

但也不要过度依赖 IF NOT EXISTS

如果表已经存在但结构不对,IF NOT EXISTS 可能会掩盖问题。

所以迁移脚本最好由工具保证只执行一次,而不是每条 SQL 都写成“随便重复执行也没事”。


六、危险变更要拆步骤

最危险的迁移通常是:

  • 删除字段
  • 改字段类型
  • 改字段含义
  • 加 NOT NULL
  • 大表加索引
  • 大表更新全量数据

不要一次性写:

1
ALTER TABLE users DROP COLUMN old_name;

更稳的方式是分阶段:

1
2
3
4
5
6
1. 新增 new_name 字段
2. 代码同时写 old_name 和 new_name
3. 后台任务回填历史数据
4. 代码切换为只读 new_name
5. 观察一段时间
6. 删除 old_name

这叫兼容式迁移。

它比“一刀切”麻烦,但生产环境安全很多。


七、大表变更要考虑锁

小表怎么改都不明显。

大表不一样。

比如千万级数据表上加字段、加索引、改类型,都可能导致锁表或长事务。

上线前至少要问:

  • 这条 DDL 会不会锁表?
  • 需要多久?
  • 有没有并发写入影响?
  • 能不能在线创建索引?
  • 是否需要分批回填?
  • 是否需要低峰期执行?

例如 PostgreSQL 创建索引可以考虑:

1
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);

不同数据库细节不一样。

但原则一样:大表迁移要先评估,不要在发布脚本里盲跑。


八、数据回填不要塞进一个巨大事务

假设要给历史用户回填状态:

1
UPDATE users SET status = 'active' WHERE status IS NULL;

如果表很大,这条 SQL 可能执行很久,产生大量锁和 WAL/binlog。

更稳的方式是分批:

1
2
3
4
UPDATE users
SET status = 'active'
WHERE id >= 1 AND id < 10001
AND status IS NULL;

应用层或脚本按 ID 范围循环执行。

每批提交一次。

这样失败后也更容易恢复。

迁移不是越自动越好。

关键是可控。


九、回滚脚本要谨慎

有些迁移工具支持 down 脚本。

比如:

1
2
3
4
5
-- up
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);

-- down
ALTER TABLE users DROP COLUMN avatar_url;

看起来很美。

但生产环境里,回滚数据库不总是安全的。

如果新字段已经写入数据,直接 drop 就会丢数据。

所以我更倾向:

1
2
3
代码可以快速回滚
数据库尽量向前修复
破坏性回滚必须人工确认

也就是说,down 脚本可以有,但不要把它当万能撤销按钮。


十、迁移要纳入发布流程

推荐发布顺序:

1
2
3
4
5
6
7
1. 备份关键数据
2. 执行兼容性数据库迁移
3. 部署新代码
4. 验证核心接口
5. 执行数据回填
6. 观察日志和指标
7. 清理旧字段或旧逻辑

很多时候,数据库迁移不应该和应用部署强绑定在同一分钟。

尤其是大表迁移和数据回填,最好拆成独立步骤。

发布越复杂,越需要清单。

不要靠临场发挥。


十一、我的建议

数据库迁移可以先从几个简单规则开始:

  • 所有结构变更都写迁移脚本
  • 迁移脚本进 Git
  • 禁止手工改生产结构后不补脚本
  • 用迁移工具记录执行版本
  • 危险变更拆成兼容步骤
  • 大表变更先评估锁和耗时
  • 数据回填分批执行
  • 回滚脚本不要盲目自动跑
  • 发布前在测试库完整演练一次

数据库迁移不是为了形式感。

它是为了让每一次结构变化都可追踪、可复现、可恢复。

项目越久,这件事越值钱。