---
title: "迁移记录显示已应用，生产环境却缺少数据库变更"
canonical: "https://siyuanailab.com/curation/translations/6e7ec564b1f4d95105b348b812d45ec4-7a3aa026e7a75972e2e1c2948b3b3755d66525043217532df44dcb29ecc15225/"
dateModified: "2026-10-06"
datePublished: "2026-10-06"
---

[返回日报条目](https://siyuanailab.com/curation/#entry/6e7ec564b1f4d95105b348b812d45ec4)

 

资料节译

 

# 迁移记录显示已应用，生产环境却缺少数据库变更

 

[阅读原文](https://dev.to/gemmein/migration-says-applied-but-production-says-column-does-not-exist-7e8)

 

现有资料无法确认原文完整，本页只翻译已取得的材料。

 

现有原件无法确认图片是否齐全。

 

我们阅读了 3,426 篇来自开发者的公开帖子，这些开发者的应用在上线时或上线后出过问题，并核实了 418 个近期案例。其中 6% 与数据库和数据丢失有关。在这些数据中，有 6 份已核实的报告描述了同一种故障：部署成功了，迁移工具显示所有内容都已应用，但生产环境并不认账。

 

从开发者的角度看，情况是这样的：部署后 API 路由立即返回 500，日志显示 `column "plan_id" does not exist`。你运行 `prisma migrate deploy` 或 `supabase db push`，它们报告没有待应用的内容。功能连续数天无法使用，因为你询问的每个工具都说架构是最新的。较隐蔽的情况是：数据迁移完成时没有报错，几周后才有人发现记录少了。丢失的几乎总是没有父记录的行，例如没有租户的事件或没有账户的订单。

 

这两种情况都源于同一个习惯：相信成功提示，而不去检查数据库。

 

你需要 Docker 和 `psql`。将以下内容保存为 `repro.sql`：

 

```
-- old schema: events.tenant_id was never a foreign key
CREATE TABLE tenants (id int PRIMARY KEY, name text NOT NULL);
CREATE TABLE events  (id int PRIMARY KEY, tenant_id int, payload text);
INSERT INTO tenants VALUES (1, 'acme');
INSERT INTO events VALUES
  (1, 1,    'signup'),
  (2, 1,    'upgrade'),
  (3, NULL, 'webhook, before tenants existed'),
  (4, 99,   'import from a deleted tenant');
-- the migration: move events into a tenant-scoped table
BEGIN;
CREATE TABLE tenant_events (
  id int PRIMARY KEY,
  tenant_id int NOT NULL REFERENCES tenants(id),
  tenant_name text NOT NULL,
  payload text
);
INSERT INTO tenant_events
SELECT e.id, e.tenant_id, t.name, e.payload
FROM events e
JOIN tenants t ON t.id = e.tenant_id;
DROP TABLE events;
COMMIT;
SELECT count(*) AS moved FROM tenant_events;
```

 

```
docker run --rm -d --name pgrepro -e POSTGRES_PASSWORD=pg -p 54329:5432 postgres:17
until pg_isready -q -h localhost -p 54329; do sleep 1; done
psql postgresql://postgres:pg@localhost:54329/postgres -v ON_ERROR_STOP=1 -f repro.sql
```

 

psql 报告 `INSERT 0 2` 和 `COMMIT`，随后 `moved` 为 2。写入了 4 条事件，却只取出了 2 条。旧表已经不见了，而 `ON_ERROR_STOP` 从未遇到错误。

 

这个例子需要一个 Prisma 项目，所以这里给出操作步骤，而不是测试夹具。一个添加 `subscriptions.plan_id` 的迁移在生产环境中失败了，`prisma migrate deploy` 现在拒绝继续应用任何内容。多数人首先会用这种方式解除阻塞：

 

```
npx prisma migrate resolve --applied 20261001120000_add_plan_id
npx prisma migrate deploy   # reports nothing to apply
npx prisma migrate status   # reports the schema as up to date, exits 0
```

 

```
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'subscriptions' AND column_name = 'plan_id';
-- (0 rows)
```

 

所有工具都显示正常。列却并不存在，每个读取该列的请求都返回 500。

 

`prisma migrate deploy` 会应用文件夹中 `_prisma_migrations` 未列为已应用的迁移。Prisma CLI 参考文档说明它不会检查偏移。`migrate status` 会比较同样的两项：迁移文件夹和数据表。Prisma 的基线设置指南称，`prisma migrate resolve --applied`“会将目标迁移添加到 `_prisma_migrations` 表中，并标记为已应用”。它只写入一行，不运行任何 SQL。迁移失败时尚未运行的语句，现在将永远不会运行。

 

Supabase 的行为也一样。`supabase db push` 会在 `supabase_migrations.schema_migrations` 中记录每次迁移，其参考文档称“后续推送会跳过已应用的迁移”。`supabase migration repair --status applied` 会直接插入这条记录。

 

历史记录表只是对架构状态的一种声明，而且部署时没有任何机制会再次检查这项声明。

 

如果管道中或运行该命令的笔记本电脑上的 `DATABASE_URL` 指向开发环境或预览分支，迁移就会在那里运行，而该数据库的历史记录看起来完美无缺。在 Vercel 上，每个变量都限定于 Production、Preview 或 Development，文档说明变更“不会应用于先前的部署，只会应用于新的部署”。更正 URL 后，只有重新部署才会生效。

 

构建和部署是自动化的，架构变更却不是。新代码上线时会依赖某个列，而该列能否创建取决于是否有人记得运行一条命令。如果开发环境和生产环境共用一个数据库，情况会更糟：每次迁移都会先对真实数据运行，因此没有一个阶段能让它安全失败。

 

内连接只返回匹配的行。`NULL = 1` 不成立，所以事件 3 永远匹配不上。没有 id 为 99 的租户，因此事件 4 也匹配不上。`INSERT ... SELECT` 插入了 `SELECT` 返回的每一行，而它只被要求做这些。

 

`UPDATE ... FROM` 的工作方式也一样，PostgreSQL 文档对此说得很清楚：“如果某个 `accounts`.`sales_person` 条目没有匹配项”，连接形式“根本不会更新该行”。命令标签会报告发生变化的行数，而计数为 0“不会被视为错误”。使用 `UPDATE` 时，被跳过的行会保持原样。像复现案例中那样，在复制后执行 `DROP`，它们就会被删除。

 

孤儿记录通常是因为旧列没有外键约束，而缺少约束往往正是最初进行迁移的原因。

 

这项测试忽略历史记录表，直接检查数据库本身。这样就能捕捉三种情况：历史记录表撒谎、数据库错误，以及迁移从未运行。保存为 `scripts/check-schema.sh`：

 

```
#!/usr/bin/env bash
# Fails if the live schema differs from prisma/schema.prisma.
# Prisma 7: the URL comes from prisma.config.ts.
set -euo pipefail
# exit 1: unapplied or failed migrations, or diverged history
npx prisma migrate status
# exit 2: the live schema differs from schema.prisma
npx prisma migrate diff --exit-code \
  --from-config-datasource \
  --to-schema=prisma/schema.prisma
```

 

如果你的 `prisma.config.ts` 读取的是 `DATABASE_URL`，请使用 CI 密钥中的生产 URL 运行它：

 

```
DATABASE_URL="$PROD_DATABASE_URL" bash scripts/check-schema.sh
```

 

执行上述 `resolve --applied` 步骤后，`migrate status` 会通过，而 `migrate diff` 会以状态码 2 退出。差异就是能发现缺口的检查。要打印缺失的 SQL，请将 `--exit-code` 换成 `--script`。

 

在 Supabase 上，使用 `supabase db diff --linked`，它会通过在单独容器中创建的影子数据库，“将本地迁移文件与已关联的项目进行差异比较”。参考文档没有说明存在差异时的退出码，因此请检查输出，并在无法确认时直接失败：

 

```
supabase migration list
out=$(supabase db diff --linked 2>&1)
grep -q "No schema changes found" <<<"$out" || { echo "$out"; exit 1; }
```

 

如果 CLI 更改了那条消息，检查就会明确报错，而不是悄悄通过；这样失败更安全。

 

用会导致连接失败的行填充一个临时数据库，然后运行迁移。测试只会在一种情况下失败：迁移成功了，但有行丢失。保存为 `scripts/test-data-migration.sh`：

 

```
#!/usr/bin/env bash
# Usage: bash scripts/test-data-migration.sh path/to/migration.sql
set -uo pipefail
DB=postgresql://postgres:pg@localhost:54329/postgres
psql "$DB" -q -v ON_ERROR_STOP=1 <<'SQL' || exit 1
DROP SCHEMA public CASCADE; CREATE SCHEMA public;
CREATE TABLE tenants (id int PRIMARY KEY, name text NOT NULL);
CREATE TABLE events  (id int PRIMARY KEY, tenant_id int, payload text);
INSERT INTO tenants VALUES (1, 'acme');
INSERT INTO events VALUES (1, 1, 'ok'), (2, NULL, 'no tenant'), (3, 99, 'dangling');
SQL
if psql "$DB" -q -v ON_ERROR_STOP=1 -f "$1"; then
  moved=$(psql "$DB" -tAc "SELECT count(*) FROM tenant_events")
  [ "$moved" = 3 ] || { echo "FAIL: migration succeeded but moved $moved of 3 events"; exit 1; }
fi
echo "PASS: every row moved, or the migration refused"
```

 

将 `repro.sql` 中的迁移部分（从 `BEGIN` 到 `COMMIT`）复制到 `move_events.sql`，然后运行：

 

```
bash scripts/test-data-migration.sh move_events.sql
# FAIL: migration succeeded but moved 1 of 3 events
```

 

引发错误的迁移会通过这项测试。它会使部署失败，但不会丢失任何数据。在真实项目中，先应用较早的迁移来构建临时架构，使用临时 URL 执行 `prisma migrate deploy`，或在本地堆栈上执行 `supabase db reset`。然后插入孤儿行。

 

**将迁移设为可导致部署失败的部署步骤。**在 Render 上，预部署命令会在构建之后、上线之前运行。“如果任何命令失败或超时，整个部署都会失败”，而上一次成功的部署会继续提供服务。在 Railway 上，预部署命令失败后“不会重试，部署也不会继续”。在其他托管平台上，使用生产部署所依赖的 CI 作业运行迁移。在同一步骤中运行架构检查，这样历史记录表撒谎也会导致部署失败：

 

```
npx prisma migrate deploy && bash scripts/check-schema.sh
```

 

**为生产环境配置独立的数据库和凭据。**为开发、预览和生产环境使用不同的数据库，并在托管平台的设置中将每个 `DATABASE_URL` 限定到对应环境。不要把生产 URL 放在本地的 `.env` 文件中，以免本地命令或编程代理读取到它。

 

**只有检查过架构后才修复历史记录。**先运行 `information_schema` 查询。如果变更已经存在，`resolve --applied` 或 `repair --status applied` 就是正确做法。如果变更不存在，修复方法取决于所用工具。在 Supabase 上，`supabase migration repair --status reverted <version>` 会删除这条记录，因此下一次 `db push` 会应用该迁移。使用 Prisma 时，创建一个包含缺失变更的新迁移并部署它。

 

**在迁移中决定如何处理孤儿记录。**你可以拒绝它们、通过 `LEFT JOIN` 和可空列保留它们，或将它们移到隔离表中。这些都是明确的决定。意外丢弃它们则不是。最简单的防护措施是统计未匹配的行并引发错误。这会回滚事务，并使部署步骤失败：

 

```
DO $$
DECLARE orphans int;
BEGIN
  SELECT count(*) INTO orphans
  FROM events e
  LEFT JOIN tenants t ON t.id = e.tenant_id
  WHERE t.id IS NULL;
  IF orphans > 0 THEN
    RAISE EXCEPTION '% events have no tenant', orphans;
  END IF;
END $$;
```

 

将它紧接着放在 `BEGIN` 之后的 `move_events.sql` 中，测试 2 就会通过。迁移会因 `2 events have no tenant` 而停止，不会丢弃任何数据。

 

**生产运行前，先阅读由代理编写的迁移。**如果 AI 编程工具编写了 SQL，请先阅读，并在暂存副本上运行。不要让代理运行环境获得生产凭据，以免它直接应用迁移。

 

- - 生产部署会在一个出错就导致部署失败的步骤中运行迁移。
 
- - 每次部署后，`prisma migrate diff --exit-code` 或`supabase db diff --linked` 都会针对生产环境运行，并报告没有变更。
 
- - `prisma migrate status` 以状态码 0 退出，或者`supabase migration list` 显示本地与远程的迁移相同。
 
- - 开发、预览和生产环境使用独立的数据库，生产 URL 只存在于生产环境的设置中。
 
- - 每个包含连接操作的数据迁移都有针对未匹配行的防护，并配有包含 NULL 父项和悬空父项的测试夹具。
 
- 任何人都不运行 `resolve --applied` 或`repair --status applied`，直到`information_schema` 查询确认变更。
 
- - 在暂存环境中，故意出错的迁移会使部署失败，且先前版本会继续提供服务。

 

报告中出现的症状及各原因对应的修复方法，都列在故障排查页面：[https://research.gemmein.com/guides/database-migration-not-applied-in-production](https://research.gemmein.com/guides/database-migration-not-applied-in-production)

 

上次迁移发布后，你凭什么判断它成功了：历史记录表，还是数据库？

 

AI 翻译；具体表述请与原文对照。
