教程 搜索 / 发布与基础设施 / 管理数据库迁移
📝 文字 ● 中级 更新于 2026-05-13

管理数据库迁移

迁移是一种版本化的 Schema 变更,可在任意环境中重放,将数据库带到特定状态。之所以重要,是因为直接在生产控制台执行临时 ALTER TABLE 会在短短一个月内让本地开发环境与生产环境的 Schema 产生偏差。迁移让 Schema 演进变得可预期、可审查。

迁移之所以作为独立概念存在——而不是"需要时直接 ALTER TABLE"——是因为 Schema 会发生漂移。开发数据库在功能开发过程中自由演进,测试数据库上个季度的演进路径又有所不同,生产数据库则按历史操作的顺序应用了各自的变更。没有统一的管理机制,"当前实际的 Schema 是什么"就只有拥有管理员权限的运维工程师才能回答,而在开发环境运行正常的变更在生产环境失败时往往原因不明。有了迁移,Schema 就是仓库中一系列版本化的文本文件;任何环境都可以通过重放对应文件升级到任意版本;只需查看迁移文件夹,就能回答"当前 Schema 是什么"这个问题。

核心概念很简单:迁移是一个文件(通常是两个——一个应用变更的 up 步骤和一个回滚变更的 down 步骤),绑定到唯一的版本号(时间戳或序列号)。迁移工具负责追踪每个数据库上已应用的迁移(通常通过 schema_migrations 表),并按顺序应用未执行的迁移。不同工具生成的文件格式各异——原生 SQL、ORM 专属 DSL、声明式 Schema 差异——但结构是一致的。选定一种工具,提交迁移文件,每次部署时运行该工具,绝不修改已发布的迁移。

本教程涵盖 2026 年值得考虑的三种工具——Prisma MigrateDrizzle Kit原生 SQL 搭配小型迁移工具——解释各自的适用场景,演示每种工具的首次迁移流程,并以防范迁移灾难的操作规范作为收尾。

你将学到什么

前提条件:一个应用连接的 Postgres 数据库(本地 Docker、自托管,或 Supabase/Neon/RDS 托管服务),一个连接它的应用,以及一个明确要实施的 Schema 变更(例如,新增一列、新增一张表、新增一个索引)。你需要在下面三种工具风格中选择一种,它们不能混用。

第一步:选择工具风格

1

这个决定决定后续的一切

  • Prisma Migrate — 在 schema.prisma 中描述 Schema,Prisma 对比数据库差异并生成 SQL 迁移文件。最适合已在使用 Prisma 作为 ORM 的项目。强大的类型生成能力;Schema、类型与数据库紧密耦合。
  • Drizzle Kit — 以 TypeScript 代码(.ts 文件中的表定义)定义 Schema,Drizzle 生成 SQL 迁移文件。耦合度低于 Prisma——生成的迁移是可读、可编辑、可独立运行的纯 SQL。可与 Drizzle ORM 配合,也可单独使用。
  • 原生 SQL + 迁移工具(node-pg-migrate、knex、sqitch、Flyway)— 手写 SQL,由迁移工具追踪已应用的迁移。不依赖任何 ORM,移植性最强;Schema 只存在于 SQL 文件和数据库本身,没有其他来源。

简单判断标准:已在使用 Prisma?选 Prisma Migrate。想要类型安全的数据库代码但迁移文件是纯 SQL?选 Drizzle Kit。追求最大移植性或技术栈不是 TypeScript?选原生 SQL。

后期切换代价较高,请慎重选择。

第二步(Prisma):编写 Schema,生成迁移

2

适用于已使用 Prisma 的项目

编辑 prisma/schema.prisma 描述目标 Schema。新增一张表的示例:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

生成并应用迁移:

npx prisma migrate dev --name add_post_table

此命令会创建 prisma/migrations/<timestamp>_add_post_table/migration.sql——Prisma 根据差异生成的可读 SQL——对开发数据库执行迁移,并重新生成 TypeScript 类型。打开该文件仔细阅读生成的内容。Prisma 的差异计算通常是正确的,但偶尔会有意外行为(将列删除后重建而不是重命名,生成你未预期的索引)——应在代码审查阶段发现,而不是等到生产环境。

将迁移文件夹提交到仓库。部署时:npx prisma migrate deploy 会对目标数据库应用所有未执行的迁移。切勿对生产环境运行 migrate dev——它可能触发数据库重置提示。

第三步(Drizzle):定义 Schema,生成迁移

3

从 TypeScript Schema 定义生成 SQL 迁移

db/schema.ts 中以 TypeScript 定义 Schema:

import { pgTable, serial, varchar, integer, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 255 }).notNull(),
  body: varchar("body").notNull(),
  authorId: integer("author_id").references(() => users.id).notNull(),
  createdAt: timestamp("created_at").defaultNow(),
});

drizzle.config.ts 中配置 Drizzle:

import { defineConfig } from "drizzle-kit";
export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL! },
});

生成迁移 SQL:

npx drizzle-kit generate
# 创建 drizzle/0001_add_posts.sql,包含实际的 SQL 语句。
# 打开它,阅读它,提交它。

# 应用迁移:
npx drizzle-kit migrate

Drizzle 的优势在于:生成的 SQL 是纯粹的。如果你不满意生成的差异,可以在提交前手动编辑 .sql 文件。migrate 命令通过 __drizzle_migrations 表追踪已应用的迁移。

第四步(原生 SQL):node-pg-migrate 或同类工具

4

自己编写 SQL,由工具追踪执行记录

对于 Node.js 项目,node-pg-migrate 是一个轻量且维护良好的迁移工具。其他技术栈也有对应工具:knex 迁移、Flyway(JVM 及独立版本)、sqitch(语言无关)、Django/Rails/Laravel 内置迁移。

npm i node-pg-migrate
npx node-pg-migrate create add-posts-table
# 创建 migrations/<timestamp>_add-posts-table.js,包含 up() 和 down() 导出。

编写 SQL:

// migrations/1715000000000_add-posts-table.js
exports.up = pgm => {
  pgm.createTable("posts", {
    id: "id",  // shorthand for serial primary key
    title: { type: "varchar(255)", notNull: true },
    body: { type: "text", notNull: true },
    author_id: { type: "integer", notNull: true, references: "users" },
    created_at: { type: "timestamp", default: pgm.func("now()") },
  });
};

exports.down = pgm => {
  pgm.dropTable("posts");
};

应用/回滚:

npx node-pg-migrate up          # 应用所有未执行的迁移
npx node-pg-migrate down        # 回滚最近一次迁移

迁移记录存储在 pgmigrations 表中。对于更高级的需求(纯 SQL,不使用 DSL),可以在 up() 中使用 pgm.sql("CREATE TABLE ...")。工具不关心内容,只负责执行你写的 SQL。

"Schema 即 TypeScript"的工具(Prisma、Drizzle)将 Schema 与特定语言栈绑定。如果你日后可能切换语言(比如 Node → Python),原生 SQL 方案是唯一能原封不动保留下来的。Schema 以纯粹的 .sql 文件形式存在,任何语言的任何工具都可以应用它们。如果你有任何理由认为技术栈可能迁移,原生 SQL 值得优先考虑。

第五步:防范迁移灾难的规则

5

三条不可违背的原则

  • 绝不修改已部署的迁移。一旦某个迁移在任何共享环境(测试环境、生产环境)执行过,它就已冻结。下一次变更必须是一个新的迁移。修改已部署的迁移意味着文件内容与已应用的状态不再一致——此后每个环境的 Schema 都会出现偏差,迁移工具无法协调。
  • 迁移应始终向前兼容。新增列时使用可空(nullable)列,新增表,新增索引。通过回滚迁移来撤销部署通常在生产环境行不通(新数据已依赖新 Schema 写入)。按只向前演进来规划。
  • 部署前进行端到端测试。启动一个干净的数据库,重放所有迁移,验证 Schema 符合预期。这正是 CI 为你做的事——参见GitHub Actions CI/CD 第六步,了解在 CI 中运行迁移的 service container 模式。

第六步:大数据量表的生产注意事项

6

在千行数据上能跑通的迁移,在亿行数据上可能崩溃

大多数迁移在小表上是即时完成的,在大表上则可能很慢甚至造成锁表。以下是需要了解的关键模式:

  • 新增带默认值的 NOT NULL 列 — 在 Postgres 11+ 上速度很快(仅修改元数据)。在旧版本上会触发全表重写。务必确认你的 Postgres 版本。
  • 新增外键约束 — 校验时会锁定两张表。大表处理方式:先以 NOT VALID 添加约束(快速,跳过校验),再在单独的迁移中执行 VALIDATE CONSTRAINT(耗时,但不阻塞写操作)。
  • 创建索引 — 默认情况下会锁定表的写操作。在 Postgres 上使用 CREATE INDEX CONCURRENTLY 进行在线构建。不能在事务内运行——需要为该迁移禁用事务包装(各工具均有对应参数)。
  • 批量回填数据 — 切勿在迁移中执行 "UPDATE big_table SET col = ...",这会导致长达数小时的表锁。应通过应用代码或独立脚本分批回填;迁移只负责新增列。

对于超大表(Postgres 上 5000 万行以上),可以研究 pg_repack 和在线 Schema 变更工具。迁移工具本身无法帮你规避锁问题,关键在于精心编写 SQL。

第七步:回滚

7

何时可以回滚,何时不行,以及替代方案

down 迁移主要在开发阶段有用——应用迁移后发现有误,回滚,重新生成。在生产环境中,"回滚迁移"通常不是正确答案:

  • 新 Schema 下已写入了数据,回滚会丢失这些数据。
  • 中途取消的长时间迁移可能使数据库处于部分变更状态,down 迁移并不知道哪些步骤已经完成。
  • 回滚生产环境的已部署迁移会造成偏差:生产在 Schema 版本 N-1,而测试环境在版本 N。针对版本 N 编写的下一个迁移将无法在生产环境应用。

生产环境的正确模式是:向前滚动,而非向后回滚。如果某个迁移部署后出现问题,就写一个新迁移来修正。删掉本不该添加的列,还原约束,诸如此类。这样可以保持迁移序列的线性,使所有环境收敛到同一个最终状态。

第八步:填充测试数据

8

不要放进迁移文件

"种子数据"——开发用的虚拟用户、查找表、演示内容——很容易被放进迁移文件,因为它离 Schema 很近。但请不要这样做。迁移应只包含 Schema;数据应放在单独的脚本中。

// scripts/seed.ts
import { db } from "./db";

await db.users.create({ email: "admin@local", role: "admin" });
await db.users.createMany([
  { email: "alice@local" },
  { email: "bob@local" },
]);

在全新数据库搭建完成后运行 npm run seed。种子脚本应是幂等的(使用 upsert 或先检查再插入),可以多次安全运行。这种分离让迁移文件保持纯 Schema,而种子数据与其代表的测试夹具放在一起。

下一步