用 AI 做数据库迁移:零停机 schema 变更工作流
适用场景
- 生产数据库需要 schema 变更
- 大表(千万行+)加列 / 改类型 / 加索引
- 需要零停机迁移
- 想让 AI 生成迁移文件 + 回滚方案
迁移原则
- 永远可回滚——每个迁移文件都有对应的 down migration
- 分阶段执行——大变更拆成多步,每步都可独立回滚
- 先兼容后破坏——先让代码兼容新 schema,再删旧字段
- AI 生成 + 人工审查——AI 写迁移,人审 SQL
工作流
需求:给 users 表加 phone 字段
→ Step 1: Claude 生成迁移文件(含 up + down)
→ Step 2: Claude 检查兼容性(是否破坏现有代码)
→ Step 3: 在 shadow DB 测试迁移
→ Step 4: 生产分阶段执行
→ Step 5: 验证 + 清理
Step 1: AI 生成迁移
用 Claude Code 连数据库(通过 MCP),描述需求:
给 users 表加一个 phone 字段,varchar(20),可空,加唯一索引。
用 Drizzle migration 格式生成,含 up 和 down。
检查现有代码是否有依赖。
Claude 会:
- 查当前 users 表结构
- 生成 Drizzle schema 改动
- 生成 SQL migration 文件
- 生成回滚 SQL
- 检查代码里的
select *和insert是否受影响
生成的文件:
-- migrations/0024_add_user_phone.sql
-- UP
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
CREATE UNIQUE INDEX idx_users_phone ON users(phone) WHERE phone IS NOT NULL;
-- DOWN
DROP INDEX IF EXISTS idx_users_phone;
ALTER TABLE users DROP COLUMN IF EXISTS phone;
Step 2: 兼容性检查
检查这次迁移会影响哪些代码:
1. 哪些 INSERT 语句需要加 phone 字段
2. 哪些 SELECT * 会导致返回字段变化
3. 哪些 API 响应 schema 会变
Claude 输出影响清单,你确认后再执行。
Step 3: 高危操作安全模式
大表加列(千万行+)
-- ❌ 错误:直接加(锁表)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- ✅ 正确:分阶段
-- Phase 1: 加可空字段(不锁表,PostgreSQL 11+)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Phase 2: 回填数据(分批,不锁表)
-- Claude 生成批处理脚本
DO $$
DECLARE
batch_size INT := 10000;
offset_val INT := 0;
BEGIN
LOOP
UPDATE users SET phone = '' WHERE id IN (
SELECT id FROM users WHERE phone IS NULL LIMIT batch_size
);
GET DIAGNOSTICS batch_size = ROW_COUNT;
EXIT WHEN batch_size = 0;
PERFORM pg_sleep(0.1); -- 给主从复制留时间
END LOOP;
END $$;
-- Phase 3: 加约束(先检查再加)
ALTER TABLE users ADD CONSTRAINT chk_phone CHECK (phone ~ '^\+?[0-9]{6,20}$') NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT chk_phone;
改字段类型
-- ❌ 错误:直接改(锁表 + 重写全表)
ALTER TABLE users ALTER COLUMN phone TYPE BIGINT USING phone::BIGINT;
-- ✅ 正确:新字段 + 回填 + 切换 + 删旧
-- Phase 1: 加新字段
ALTER TABLE users ADD COLUMN phone_int BIGINT;
-- Phase 2: 双写(代码同时写旧和新)
-- Claude 生成代码改动:INSERT/UPDATE 同时写 phone 和 phone_int
-- Phase 3: 回填
UPDATE users SET phone_int = phone::BIGINT WHERE phone_int IS NULL AND phone ~ '^[0-9]+$';
-- Phase 4: 验证数据一致
SELECT COUNT(*) FROM users WHERE phone IS NOT NULL AND phone_int IS NULL;
-- 必须为 0
-- Phase 5: 代码切到读 phone_int
-- Phase 6: 删旧字段(等一个发布周期后)
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users RENAME COLUMN phone_int TO phone;
加索引
-- ❌ 错误:直接加(锁表写操作)
CREATE INDEX idx_users_email ON users(email);
-- ✅ 正确:CONCURRENTLY(不锁表,但慢)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- 注意:CONCURRENTLY 不能在事务里跑
Step 4: 执行 + 监控
# 1. 在 shadow DB 测试
psql $SHADOW_DB -f migrations/0024_add_user_phone.sql
# 2. 生产执行(维护窗口)
psql $PROD_DB -f migrations/0024_add_user_phone.sql
# 3. 监控(Claude 帮你写监控脚本)
watch -n 5 'psql $PROD_DB -c "
SELECT
count(*) AS total,
count(phone) AS with_phone,
count(*) - count(phone) AS without_phone
FROM users
"'
Step 5: 回滚方案
每个迁移执行前,Claude 生成回滚 checklist:
## 回滚步骤(如需)
1. 确认 down migration 安全:
```sql
SELECT count(*) FROM users WHERE phone IS NOT NULL;
-- 如果 > 0,回滚会丢数据,确认是否可接受
- 执行回滚:
psql $PROD_DB -f migrations/0024_add_user_phone_down.sql - 代码回滚到上一个版本:
git revert <merge-commit> - 验证:
\d users -- 确认 phone 字段已删
## 踩坑记录
1. **PostgreSQL 11+ 加可空字段才是即时**——低版本加字段仍会锁表,先升级。
2. **CONCURRENTLY 索引失败要手动清理**——失败后留 invalid index,`DROP INDEX` 后重建。
3. **回填脚本要分批 + sleep**——大批量 UPDATE 会撑爆 WAL 和主从延迟。
4. **NOT VALID + VALIDATE 两步走**——直接加 CHECK 会全表扫描锁表。
5. **Claude 生成 SQL 必须 review**——AI 偶尔会忘加 WHERE 条件,删数据操作尤其要审。