Communitygithub.com

数据库迁移

针对 PostgreSQL、MySQL 及常见 ORM(Prisma、Drizzle、Kysely、Django、TypeORM、golang-migrate)的数据库迁移最佳实践,涵盖 schema 变更、数据迁移、回滚和零停机部署。在编写 schema 或数据迁移、规划回滚或目标是零停机部署时使用。

数据库迁移 是什么?

该技能提供了生产级数据库迁移的全面指南,遵循“每次变更都是迁移”、“迁移不可变”等核心原则。它覆盖了 PostgreSQL 的安全模式变更模式(如并发索引、无锁添加列、分页批量更新),并为 Prisma、Drizzle、Kysely、Django 和 golang-migrate 提供了具体的 CLI 工作流和代码示例。此外,还详细说明了零停机部署的扩展-收缩模式(expand-contract pattern),帮助开发者分阶段安全地变更 schema。该技能适用于需要确保数据完整性、最小化生产环境停机时间并避免迁移反模式的工程团队。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/database-migrations

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

数据库迁移模式

面向生产系统的安全、可逆的数据库 schema 变更。

何时激活

  • 创建或修改数据库表
  • 添加/移除列或索引
  • 运行数据迁移(回填、转换)
  • 规划零停机 schema 变更
  • 为新项目设置迁移工具

核心原则

  1. 每次变更都是一次迁移——切勿手动修改生产数据库
  2. 迁移在生产环境中只进不退——回滚使用新的前向迁移
  3. schema 和数据迁移分开——切勿在一次迁移中混用 DDL 和 DML
  4. 针对生产规模的数据测试迁移——在 100 行数据上正常的迁移可能在 1000 万行上锁死
  5. 迁移一旦部署即不可变——切勿编辑已在生产环境中运行过的迁移

迁移安全检查清单

在应用任何迁移之前:

  • 迁移同时包含 UP 和 DOWN(或明确标记为不可逆)
  • 大表上无全表锁(使用并发操作)
  • 新列有默认值或可为 null(切勿在没有默认值的情况下添加 NOT NULL)
  • 索引并发创建(对于现有表,不与 CREATE TABLE 内联)
  • 数据回填与 schema 变更是独立的迁移
  • 已针对生产数据副本测试
  • 回滚计划已记录

PostgreSQL 模式

安全添加列

-- 好:可为 null 的列,无锁
ALTER TABLE users ADD COLUMN avatar_url TEXT;

-- 好:带默认值的列(Postgres 11+ 即时生效,无重写)
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;

-- 坏:对现有表添加无默认值的 NOT NULL(需要完全重写)
ALTER TABLE users ADD COLUMN role TEXT NOT NULL;
-- 这将锁定表并重写每一行

无停机添加索引

-- 坏:在大表上阻塞写入
CREATE INDEX idx_users_email ON users (email);

-- 好:非阻塞,允许并发写入
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);

-- 注意:CONCURRENTLY 不能在事务块内运行
-- 大多数迁移工具需要特殊处理

重命名列(零停机)

切勿在生产中直接重命名。使用扩展-收缩模式:

-- 步骤 1:添加新列(迁移 001)
ALTER TABLE users ADD COLUMN display_name TEXT;

-- 步骤 2:回填数据(迁移 002,数据迁移)
UPDATE users SET display_name = username WHERE display_name IS NULL;

-- 步骤 3:更新应用代码以同时读写两列
-- 部署应用变更

-- 步骤 4:停止写入旧列,删除它(迁移 003)
ALTER TABLE users DROP COLUMN username;

安全移除列

-- 步骤 1:移除对该列的所有应用引用
-- 步骤 2:部署不含该列引用的应用
-- 步骤 3:在下一个迁移中删除列
ALTER TABLE orders DROP COLUMN legacy_status;

-- 对于 Django:使用 SeparateDatabaseAndState 从模型中移除此列
-- 而不生成 DROP COLUMN(然后在下一个迁移中删除)

大数据迁移

-- 坏:在单个事务中更新所有行(锁定表)
UPDATE users SET normalized_email = LOWER(email);

-- 好:带进度的批量更新
DO $$
DECLARE
  batch_size INT := 10000;
  rows_updated INT;
BEGIN
  LOOP
    UPDATE users
    SET normalized_email = LOWER(email)
    WHERE id IN (
      SELECT id FROM users
      WHERE normalized_email IS NULL
      LIMIT batch_size
      FOR UPDATE SKIP LOCKED
    );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    RAISE NOTICE 'Updated % rows', rows_updated;
    EXIT WHEN rows_updated = 0;
    COMMIT;
  END LOOP;
END $$;

Prisma (TypeScript/Node.js)

工作流

# 从 schema 变更创建迁移
npx prisma migrate dev --name add_user_avatar

# 在生产环境中应用待处理的迁移
npx prisma migrate deploy

# 重置数据库(仅开发环境)
npx prisma migrate reset

# schema 变更后生成客户端
npx prisma generate

Schema 示例

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  avatarUrl String?  @map("avatar_url")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
  orders    Order[]

  @@map("users")
  @@index([email])
}

自定义 SQL 迁移

对于 Prisma 无法表达的操作(并发索引、数据回填):

# 创建空迁移,然后手动编辑 SQL
npx prisma migrate dev --create-only --name add_email_index
-- migrations/20240115_add_email_index/migration.sql
-- Prisma 无法生成 CONCURRENTLY,因此我们手动编写
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email);

Drizzle (TypeScript/Node.js)

工作流

# 从 schema 变更生成迁移
npx drizzle-kit generate

# 应用迁移
npx drizzle-kit migrate

# 直接推送 schema(仅开发环境,无迁移文件)
npx drizzle-kit push

Schema 示例

import { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: uuid("id").primaryKey().defaultRandom(),
  email: text("email").notNull().unique(),
  name: text("name"),
  isActive: boolean("is_active").notNull().default(true),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

Kysely (TypeScript/Node.js)

工作流 (kysely-ctl)

# 初始化配置文件 (kysely.config.ts)
kysely init

# 创建新的迁移文件
kysely migrate make add_user_avatar

# 应用所有待处理的迁移
kysely migrate latest

# 回滚最后一个迁移
kysely migrate down

# 显示迁移状态
kysely migrate list

迁移文件

// migrations/2024_01_15_001_create_user_profile.ts
import { type Kysely, sql } from 'kysely'

// 重要:始终使用 Kysely<any>,而不是你定义的 DB 接口。
// 迁移在时间上是冻结的,不能依赖当前的 schema 类型。
export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('user_profile')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
    .addColumn('avatar_url', 'text')
    .addColumn('created_at', 'timestamp', (col) =>
      col.defaultTo(sql`now()`).notNull()
    )
    .execute()

  await db.schema
    .createIndex('idx_user_profile_avatar')
    .on('user_profile')
    .column('avatar_url')
    .execute()
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('user_profile').execute()
}

编程式迁移器

import { Migrator, FileMigrationProvider } from 'kysely'
import { promises as fs } from 'fs'
import * as path from 'path'
// ESM 专用 — CJS 可以直接使用 __dirname
import { fileURLToPath } from 'url'
const migrationFolder = path.join(
  path.dirname(fileURLToPath(import.meta.url)),
  './migrations',
)

// `db` 是你的 Kysely<any> 数据库实例
const migrator = new Migrator({
  db,
  provider: new FileMigrationProvider({
    fs,
    path,
    migrationFolder,
  }),
  // 警告:仅开发环境启用。会禁用时间戳顺序验证,
  // 可能导致环境之间的 schema 漂移。
  // allowUnorderedMigrations: true,
})

const { error, results } = await migrator.migrateToLatest()

results?.forEach((it) => {
  if (it.status === 'Success') {
    console.log(`migration "${it.migrationName}" executed successfully`)
  } else if (it.status === 'Error') {
    console.error(`failed to execute migration "${it.migrationName}"`)
  }
})

if (error) {
  console.error('migration failed', error)
  process.exit(1)
}

Django (Python)

工作流

# 从模型变更生成迁移
python manage.py makemigrations

# 应用迁移
python manage.py migrate

# 显示迁移状态
python manage.py showmigrations

# 为自定义 SQL 生成空迁移
python manage.py makemigrations --empty app_name -n description

数据迁移

from django.db import migrations

def backfill_display_names(apps, schema_editor):
    User = apps.get_model("accounts", "User")
    batch_size = 5000
    users = User.objects.filter(display_name="")
    while users.exists():
        batch = list(users[:batch_size])
        for user in batch:
            user.display_name = user.username
        User.objects.bulk_update(batch, ["display_name"], batch_size=batch_size)

def reverse_backfill(apps, schema_editor):
    pass  # 数据迁移,无需反向

class Migration(migrations.Migration):
    dependencies = [("accounts", "0015_add_display_name")]

    operations = [
        migrations.RunPython(backfill_display_names, reverse_backfill),
    ]

SeparateDatabaseAndState

从 Django 模型中移除一列,但暂不从数据库中删除:

class Migration(migrations.Migration):
    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(model_name="user", name="legacy_field"),
            ],
            database_operations=[],  # 暂不修改数据库
        ),
    ]

golang-migrate (Go)

工作流

# 创建迁移对
migrate create -ext sql -dir migrations -seq add_user_avatar

# 应用所有待处理的迁移
migrate -path migrations -database "$DATABASE_URL" up

# 回滚最后一个迁移
migrate -path migrations -database "$DATABASE_URL" down 1

# 强制版本(修复脏状态)
migrate -path migrations -database "$DATABASE_URL" force VERSION

迁移文件

-- migrations/000003_add_user_avatar.up.sql
ALTER TABLE users ADD COLUMN avatar_url TEXT;
CREATE INDEX CONCURRENTLY idx_users_avatar ON users (avatar_url) WHERE avatar_url IS NOT NULL;

-- migrations/000003_add_user_avatar.down.sql
DROP INDEX IF EXISTS idx_users_avatar;
ALTER TABLE users DROP COLUMN IF EXISTS avatar_url;

零停机迁移策略

对于关键的生产变更,遵循扩展-收缩模式:

阶段 1:扩展 (EXPAND)
  - 添加新列/表(可为 null 或带默认值)
  - 部署:应用同时写入新旧两列
  - 回填现有数据

阶段 2:迁移 (MIGRATE)
  - 部署:应用从新列读取,同时写入新旧两列
  - 验证数据一致性

阶段 3:收缩 (CONTRACT)
  - 部署:应用仅使用新列
  - 在单独的迁移中删除旧列/表

时间线示例

第 1 天:迁移添加 new_status 列(可为 null)
第 1 天:部署应用 v2 — 同时写入 status 和 new_status
第 2 天:对现有行运行回填迁移
第 3 天:部署应用 v3 — 仅从 new_status 读取
第 7 天:迁移删除旧的 status 列

反模式

反模式为何失败更好的方法
在生产环境手动执行 SQL无审计跟踪,不可重复始终使用迁移文件
编辑已部署的迁移导致环境间漂移创建新迁移代替
无默认值的 NOT NULL锁定表,重写所有行添加可为 null,回填,然后添加约束
在大表上内联创建索引构建期间阻塞写入CREATE INDEX CONCURRENTLY
schema 和数据在同一个迁移中难以回滚,事务时间长分离迁移
在删除代码之前删除列应用因缺少列而报错先移除代码,下次部署再删列

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能