# 数据库变更

> 改表结构期间，旧代码照常运行。

工程防护 · /zh/migrations/ · 2026-11-05

回滚代码救不回已删除的列，也撤销不了回填。

## 让 AI agent 规划迁移

**Prompt**

```text
审查 [Jira 编号、PR 链接或 diff] 里的表结构和数据变更。
如果是 Jira ticket，通过已连接的 Jira MCP 或已认证的 acli 读取描述、验收标准和评论；访问不了就让我粘贴详情。
阅读仓库里的迁移和部署说明。拆出几个阶段，保证新旧应用版本都能正常工作，数据库操作的规模可控，并写明每个阶段怎么撤销。
列出各个阶段、进入下一阶段前需要的证据，以及所有不可逆的操作。
说明本页列出的测试哪些已在项目测试套件里。能自动化的补上，其余的列出来。
不要运行迁移或回填。
```

## 分几次发布修改一列

- **MIG-1** (高风险; 由代码评审检查): **旧代码必须能在新表结构上运行。** 先迁移，再部署依赖它的代码。

  注意: 整个滚动部署期间都要成立，因为这时新旧版本同时在跑。
- **MIG-2** (高风险; 由代码评审检查): **绝不在一次发布里重命名或删除。** 扩展、切换、收缩各自单独发布；回填在扩展和切换之间运行。等运行中的代码都不再用旧列，才能删掉它。

例如，用 `full_name` 替换 `display_name`：

1. **扩展** 新增 `full_name`，两列同时写。
2. **回填** 分批复制旧值，中断后能继续跑，绝不覆盖更新的值，直到每一行都一致。
3. **切换** 改为读 `full_name`。只要还可能回滚，就继续两列同时写。
4. **收缩** 停止使用 `display_name`（Rails 里用 [`ignored_columns`](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-ignored_columns-3D)），下一次发布再删掉它。

- [ ] **MIG-T3**: 上一版本的应用在新表结构上仍能通过冒烟测试。

## 让每个迁移都能安全部署

- **MIG-5** (高风险; 由代码评审检查): **迁移每次部署只跑一次，作为独立步骤。** 绝不放进应用的启动命令。

  注意: 否则每个实例、每次重启都会走迁移流程，互相抢锁。

* 给迁移设一个较短的锁超时：在繁忙的表上等锁的迁移会直接放弃，而不是把后面所有查询都堵住。参见 [PostgreSQL](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT)。
* 不在事务里运行的迁移（例如并发建索引），中途失败后必须能安全重跑。并发建索引失败后，会留下一个无效索引。[PostgreSQL](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY)。
* 回填作为可续跑的任务运行，不放进表结构迁移。

- [ ] **MIG-T1**: 涉及大表或繁忙的表时，先在 staging 用生产规模的数据跑一遍迁移，把耗时和加锁情况写进发布记录。

## 删除前，确认影响范围

- 预览要删的确切行和数量，包括关联行、文件和排队中的任务。检查租户范围和级联删除。
- 定好这次是软删除（隐藏）还是永久删除，写明哪些能恢复、从哪个恢复点恢复。
- 大批量删除要分批、可重新开始；数量和预期不符就停下。生产环境的删除需要[明确批准](/zh/security/#r-ai-2)。
