数据库迁移指南

为什么数据库迁移至关重要

数据库迁移(Database Migration)是应用程序生命周期中最关键也最高风险的操作之一。每次修改表结构、添加索引或调整约束,都可能影响数据完整性、查询性能甚至导致服务不可用。没有版本化的迁移管理,团队很容易陷入"在我机器上能跑"的困境,生产环境与开发环境的数据库结构逐渐分裂。本指南涵盖 12 款主流迁移工具对比、零停机策略、PostgreSQL / MySQL 实战语法、完整迁移清单和常见陷阱,帮助你安全、可靠地管理数据库变更。

迁移工具对比(12 款)

工具语言方式回滚支持最适合
FlywayJava / CLI版本化 SQL 文件(V1__name.sql)付费版 UndoJava 应用、SQL 优先团队
LiquibaseJava / CLIXML / YAML / SQL 变更日志自动 rollback多数据库、复杂变更集
golang-migrateGo / CLI编号 .up.sql / .down.sqldown 文件手写Go 应用、简单 SQL 迁移
AlembicPythonPython 脚本含 upgrade / downgradedowngrade 函数SQLAlchemy / Python 项目
Prisma MigrateTypeScript基于 Schema Diff 自动生成手动编写 downNode.js / TypeScript 应用
AtlasGo / CLI声明式(目标状态)或版本化自动生成逆向现代 Go 应用、CI/CD
Knex.jsJavaScriptJS/TS 迁移函数(up / down)down 函数手写Node.js / Express 项目
TypeORMTypeScript自动检测实体变更生成迁移down 函数自动生成TypeScript 全栈应用
SequelizeJavaScriptJS 迁移文件(up / down)down 函数手写Node.js 传统项目
Django MigrationsPython从 Model 自动检测生成迁移自动生成逆向Django Web 应用
Rails ActiveRecordRubyDSL 迁移文件(change / up / down)自动可逆或 downRuby on Rails 项目
dbmateGo / CLI纯 SQL .up.sql / .down.sqldown 文件手写轻量级、语言无关项目

迁移文件目录结构

golang-migrate(编号 up/down)

db/migrations/ ├── 000001_create_users.up.sql ├── 000001_create_users.down.sql ├── 000002_add_email_index.up.sql ├── 000002_add_email_index.down.sql ├── 000003_add_orders_table.up.sql └── 000003_add_orders_table.down.sql

Flyway(V__ / R__ / U__)

sql/ ├── V001__create_users.sql -- 版本化(按序执行一次) ├── V002__add_orders_table.sql ├── R__refresh_materialized_view.sql -- 可重复(每次内容变更时重新执行) └── U001__undo_create_users.sql -- 撤销(付费版功能)

Prisma(自动生成)

prisma/ ├── schema.prisma -- 声明式 Schema └── migrations/ ├── 20260101120000_init/ │ └── migration.sql ├── 20260115090000_add_orders/ │ └── migration.sql └── migration_lock.toml

Django(自动生成)

myapp/migrations/ ├── __init__.py ├── 0001_initial.py -- 从 models.py 自动检测 ├── 0002_add_email_field.py └── 0003_create_order_model.py # 生成迁移:python manage.py makemigrations # 执行迁移:python manage.py migrate # 查看状态:python manage.py showmigrations

常用迁移操作(PostgreSQL & MySQL)

创建表

-- PostgreSQL CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), total NUMERIC(12,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- MySQL CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, total DECIMAL(12,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

添加列(带默认值 — 零停机安全)

-- PostgreSQL(PG 11+ 添加带默认值的列不锁表) ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT ''; -- MySQL(8.0+ INSTANT 方式,几乎无锁) ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT '', ALGORITHM=INSTANT;

删除列(扩展-收缩)

-- 第 1 步:部署新代码,不再读取 old_column -- 第 2 步:确认无活跃查询引用 old_column -- 第 3 步: -- PostgreSQL ALTER TABLE users DROP COLUMN old_column; -- MySQL ALTER TABLE users DROP COLUMN old_column;

重命名列(扩展-收缩)

-- 不要直接 RENAME(会立刻中断应用) -- 正确做法:新增列 → 回填 → 双写 → 切读 → 删旧列 -- 第 1 步 ALTER TABLE users ADD COLUMN display_name VARCHAR(100); -- 第 2 步 UPDATE users SET display_name = username WHERE display_name IS NULL; -- 第 3-4 步:部署双写代码,切换读取到 display_name -- 第 5 步 ALTER TABLE users DROP COLUMN username;

添加索引(不锁表)

-- PostgreSQL — CONCURRENTLY 不阻塞写入 CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id); -- MySQL — INPLACE 方式(8.0+),允许并发 DML ALTER TABLE orders ADD INDEX idx_orders_user_id (user_id), ALGORITHM=INPLACE, LOCK=NONE;

修改列类型

-- PostgreSQL(小表可直接修改;大表建议新增列 + 回填) ALTER TABLE products ALTER COLUMN price TYPE NUMERIC(14,2); -- MySQL(大表使用 pt-online-schema-change) -- 直接方式(会锁表!仅小表使用): ALTER TABLE products MODIFY COLUMN price DECIMAL(14,2);

添加外键

-- PostgreSQL — NOT VALID 先创建约束不检查旧数据,再异步验证 ALTER TABLE orders ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID; ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user; -- MySQL ALTER TABLE orders ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id);

零停机迁移策略

扩展-收缩模式(Expand-Contract)

这是最通用的零停机方案,分 5 步完成:

步骤操作说明
1. 扩展添加新列/表只增不删,不影响现有代码
2. 回填迁移旧数据到新结构可分批处理避免锁定
3. 双写部署代码同时写入新旧结构保证迁移期间数据一致
4. 切读部署代码从新结构读取旧结构降级为备用
5. 收缩删除旧列/表确认无回退需求后执行

蓝绿数据库部署

维护两套数据库(Blue / Green)。在 Green 库执行迁移,测试通过后切换流量。适合大版本升级,但需要双倍存储和数据同步机制(如逻辑复制)。

Online DDL 工具

工具数据库原理
pt-online-schema-changeMySQL创建影子表 → 安装触发器 → 拷贝数据 → 原子重命名
gh-ostMySQL基于 binlog 复制变更,不使用触发器,更安全
pg_repackPostgreSQL在线重建表和索引,释放膨胀空间
pgrollPostgreSQL多版本 Schema 共存,渐进式迁移

影子表方式(Shadow Table)

-- 1. 创建与原表结构相同的影子表(含新结构变更) CREATE TABLE users_new (LIKE users INCLUDING ALL); ALTER TABLE users_new ADD COLUMN display_name VARCHAR(100); -- 2. 安装触发器同步增量数据 -- 3. 批量拷贝旧数据到影子表 -- 4. 原子交换表名 ALTER TABLE users RENAME TO users_old; ALTER TABLE users_new RENAME TO users; -- 5. 删除旧表 DROP TABLE users_old;

迁移执行清单

阶段操作说明
迁移前全量备份数据库pg_dump / mysqldump / 快照
迁移前在 staging 环境用生产数据副本测试确保迁移脚本在真实数据上正确运行
迁移前评估 DDL 变更的锁持有时长对大表使用 pg_stat_activity 检查活跃锁
迁移前准备回滚脚本并验证可用性在 staging 先跑一遍 down 迁移
迁移前通知团队迁移时间窗口协调发布冻结期
迁移中监控活跃连接数和主从延迟延迟 > 阈值则暂停
迁移中设置 statement_timeout / lock_timeout避免无限等锁导致连接堆积
迁移中管理连接池必要时临时扩大连接池或暂停非关键服务
迁移后验证行数和数据完整性对比迁移前后的 COUNT / CHECKSUM
迁移后监控慢查询和索引使用情况检查 pg_stat_user_indexes 或 EXPLAIN
迁移后性能基准测试对比核心查询响应时间
收尾确认旧代码完全下线后再删废弃列保留至少 1-2 个发布周期

常见陷阱

  • 直接在生产环境跑未测试的迁移 — 永远先在 staging 用生产数据副本验证。一条有语法错误的 SQL 可能留下半完成的迁移状态。
  • 没有回滚脚本 — 每个 up 迁移都应有对应的 down。发现问题时没有回滚脚本,只能手动修复,风险极高。
  • 大表长时间锁定 — ALTER TABLE 在大表上可能持锁数分钟甚至数小时。使用 CONCURRENTLY、pt-online-schema-change 或 gh-ost 避免。
  • 不可逆的数据丢失 — DROP COLUMN 不可恢复。确保删列前数据已迁移到新列,并保留备份。
  • 添加 NOT NULL 列不带默认值 — 会导致旧行无法满足约束。先 ADD COLUMN 带 DEFAULT,再根据需要修改。
  • 忘记更新 ORM 模型 — 迁移修改了数据库结构,但代码中的实体/模型未同步更新,导致运行时错误。
  • 迁移顺序冲突 — 多人并行开发时,迁移编号冲突或依赖顺序错误。使用时间戳命名而非顺序编号可缓解。

工具快速入门

golang-migrate

# 安装 go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest # 创建迁移 migrate create -ext sql -dir db/migrations -seq create_users # 执行迁移 migrate -path db/migrations -database "postgres://user:pass@localhost/mydb?sslmode=disable" up # 回滚一步 migrate -path db/migrations -database "postgres://..." down 1 # 查看当前版本 migrate -path db/migrations -database "postgres://..." version # 强制版本号(修复 dirty 状态) migrate -path db/migrations -database "postgres://..." force 3

Flyway

# 安装(macOS) brew install flyway # 配置 flyway.conf flyway.url=jdbc:postgresql://localhost:5432/mydb flyway.user=postgres flyway.password=secret flyway.locations=filesystem:./sql # 执行迁移 flyway migrate # 查看状态 flyway info # 验证已执行迁移与本地文件一致 flyway validate # 清空数据库(危险!仅开发环境) flyway clean # 创建基准版本(已有数据库接入 Flyway) flyway baseline

Prisma Migrate

# 安装 npm install prisma --save-dev # 编辑 prisma/schema.prisma,修改 model 后: # 生成迁移文件(开发环境) npx prisma migrate dev --name add_orders_table # 生产环境部署迁移 npx prisma migrate deploy # 查看迁移状态 npx prisma migrate status # 重置数据库(危险!删除全部数据) npx prisma migrate reset # 生成 Prisma Client npx prisma generate

Alembic

# 安装 pip install alembic # 初始化 alembic init alembic # 自动生成迁移(基于 SQLAlchemy Model 差异) alembic revision --autogenerate -m "add orders table" # 执行迁移到最新 alembic upgrade head # 回滚一步 alembic downgrade -1 # 查看当前版本 alembic current # 查看迁移历史 alembic history --verbose

常见问题 (FAQ)

Q: 迁移失败了怎么办?

A: 首先检查数据库当前状态(是否部分执行)。如果使用支持事务 DDL 的数据库(如 PostgreSQL),失败的迁移会自动回滚。MySQL 的 DDL 不支持事务,需要手动修复。大多数工具有 dirty 状态标记,修复后需要手动设置版本号(如 migrate force N)。

Q: 应该用自动生成还是手写迁移?

A: 取决于团队和场景。自动生成(Prisma、Django、Alembic autogenerate)适合快速迭代,但可能遗漏数据迁移逻辑。手写 SQL 迁移对性能敏感操作(索引、大表变更)更精确可控。推荐:自动生成 + 人工审查后再提交。

Q: 如何做数据迁移(不仅是 Schema 变更)?

A: 数据迁移应与 Schema 迁移分开管理。先做 Schema 变更(添加新列),再用独立脚本或迁移步骤回填数据。大数据量回填建议分批处理(每批 1000-10000 行),避免长事务和锁争用。

Q: CI/CD 中如何管理迁移?

A: 在部署管道中,迁移应在应用启动前执行。推荐流程:CI 阶段用临时数据库验证迁移(up → down → up),CD 阶段先执行 migrate up,成功后再滚动更新应用容器。使用 Kubernetes 时可配置 init container 执行迁移。

Q: 声明式迁移和版本化迁移怎么选?

A: 声明式(Atlas、Prisma)定义期望的最终状态,工具自动计算 diff — 适合新项目和快速原型。版本化(Flyway、golang-migrate)显式控制每一步变更 — 适合生产环境要求审计追踪和精确控制的场景。许多团队在开发阶段用声明式,生产部署时导出为版本化迁移。