第 20 课:数据库模式 — 设计、迁移与优化

所属阶段:第四阶段「语言与框架」(第 17-22 课) 前置条件:第 17 课(后端语言) 本课收获:体验一次 Migration 辅助,理解零停机迁移流程


一、本课概述

数据库是大多数应用的核心。一个糟糕的 Schema 设计会让整个系统变慢,一次不安全的 Migration 会让生产环境宕机。ECC 提供了从 Schema 设计到查询优化到零停机迁移的完整 Skill 支持。

本课回答三个问题:

  1. ECC 有哪些数据库 Skill? --- 覆盖 OLTP 和 OLAP 场景
  2. 零停机迁移怎么做? --- 五步安全迁移法
  3. database-reviewer Agent 能帮什么忙? --- 自动化数据库审查

二、数据库 Skill 全景

2.1 完整 Skill 清单

Skill 定位 核心内容
postgres-patterns PostgreSQL 核心 查询优化、Schema 设计、索引、RLS、连接池
clickhouse-io 分析型数据库 分析型查询、表引擎选择、数据摄取
database-migrations 迁移管理 零停机迁移、回滚策略、跨 ORM 支持
jpa-patterns JPA/Hibernate 实体设计、关系映射、N+1 防护
kotlin-exposed-patterns Exposed ORM DSL 查询、事务管理、HikariCP 连接池

2.2 OLTP vs OLAP

lua 复制代码
OLTP(在线事务处理)          OLAP(在线分析处理)
postgres-patterns             clickhouse-io
├── 行存储                    ├── 列存储
├── 单行读写快                ├── 聚合查询快
├── 事务保证(ACID)          ├── 最终一致性
├── 索引优化                  ├── 表引擎选择
└── 适合:业务系统            └── 适合:数据分析

三、postgres-patterns 核心

3.1 Schema 设计原则

postgres-patterns Skill 强调以下设计原则:

选择正确的数据类型

需求 推荐类型 不推荐 原因
主键 UUIDBIGSERIAL SERIAL SERIAL 在分布式场景不够用
时间戳 TIMESTAMPTZ TIMESTAMP 不带时区的时间戳是灾难
金额 NUMERIC(19,4) FLOAT 浮点数有精度问题
状态枚举 TEXT + CHECK ENUM 类型 ENUM 修改需要 ALTER TYPE
JSON 数据 JSONB JSON JSONB 支持索引和查询

表设计清单

sql 复制代码
-- 推荐的表结构模板
CREATE TABLE orders (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id),
    status      TEXT NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered')),
    total       NUMERIC(19, 4) NOT NULL CHECK (total >= 0),
    metadata    JSONB DEFAULT '{}',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 必备索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status) WHERE status != 'delivered';
CREATE INDEX idx_orders_created_at ON orders(created_at);

3.2 查询优化

EXPLAIN ANALYZE 是你的最佳朋友

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE user_id = '...' AND status = 'pending'
ORDER BY created_at DESC
LIMIT 20;

关键指标解读

指标 需要关注 危险
Seq Scan 小表 (<1K 行) 中表 (1K-100K) 大表 (>100K)
Index Scan 总是好的 --- ---
Nested Loop 小数据集 --- 大数据集内层无索引
Sort (external) --- --- 内存不足导致磁盘排序

3.3 索引策略

sql 复制代码
索引选择决策树:

等值查询 (WHERE col = ?)
  → B-tree 索引(默认)

范围查询 (WHERE col BETWEEN ? AND ?)
  → B-tree 索引

全文搜索 (WHERE col @@ to_tsquery(?))
  → GIN 索引

JSONB 查询 (WHERE col @> '{"key": "value"}')
  → GIN 索引

地理位置 (WHERE ST_DWithin(col, point, distance))
  → GiST 索引

部分数据 (WHERE status = 'active')
  → 部分索引 (Partial Index)

3.4 RLS(Row Level Security)

sql 复制代码
-- 启用 RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- 用户只能看自己的文档
CREATE POLICY user_documents ON documents
    FOR SELECT
    USING (user_id = current_setting('app.current_user_id')::UUID);

-- 管理员可以看所有文档
CREATE POLICY admin_documents ON documents
    FOR ALL
    USING (current_setting('app.current_role') = 'admin');

3.5 连接池

ini 复制代码
应用层连接池配置要点:

最大连接数 = CPU 核数 * 2 + 磁盘数
  (PostgreSQL 官方推荐公式)

示例:4 核 CPU + 1 SSD
  最大连接数 = 4 * 2 + 1 = 9

常见错误:
  ✗ max_connections = 100(每个应用实例)× 10 个实例 = 1000 连接
  ✓ 使用 PgBouncer 做连接池代理,应用层连接到 PgBouncer

四、clickhouse-io(分析型查询)

4.1 何时使用 ClickHouse

场景 PostgreSQL ClickHouse
用户订单 CRUD 适合 不适合
实时仪表盘 勉强 非常适合
日志分析 不适合 非常适合
时序数据 可以 更好
事务处理 适合 不适合

4.2 表引擎选择

clickhouse-io Skill 中最重要的决策是表引擎选择:

复制代码
MergeTree         --- 默认选择,适合大多数场景
ReplacingMergeTree --- 需要去重时使用
SummingMergeTree   --- 需要预聚合时使用
AggregatingMergeTree --- 复杂聚合场景

4.3 数据摄取模式

sql 复制代码
批量插入 > 逐行插入

✗ 逐行插入(每秒数百行)
  INSERT INTO events VALUES (...)  -- 每次一行

✓ 批量插入(每秒数百万行)
  INSERT INTO events VALUES
    (...), (...), (...), ...        -- 每次数千行

✓ 异步插入
  INSERT INTO events SETTINGS async_insert = 1
  VALUES (...)                      -- 自动批量化

五、database-migrations 核心

5.1 零停机迁移五步法

database-migrations Skill 定义了零停机迁移的标准流程。核心原则:每次只做一件事

以"重命名列"为例(看似简单,实际很危险):

sql 复制代码
直接做法(会宕机):
  ALTER TABLE users RENAME COLUMN name TO full_name;
  → 应用代码还在读 name → 报错 → 宕机

零停机五步法:

步骤 1:加新列
  ALTER TABLE users ADD COLUMN full_name TEXT;

步骤 2:双写
  部署代码:同时写 name 和 full_name

步骤 3:迁移数据
  UPDATE users SET full_name = name WHERE full_name IS NULL;

步骤 4:切读
  部署代码:读 full_name,仍然双写

步骤 5:删旧列
  ALTER TABLE users DROP COLUMN name;

5.2 每步一个 Migration 文件

bash 复制代码
migrations/
├── 001_add_full_name_column.sql      # 步骤 1
├── 002_backfill_full_name.sql        # 步骤 3(步骤 2 是代码变更)
└── 003_drop_name_column.sql          # 步骤 5(步骤 4 是代码变更)

5.3 回滚策略

每个 Migration 必须有对应的回滚

sql 复制代码
-- 001_add_full_name_column.sql
-- UP
ALTER TABLE users ADD COLUMN full_name TEXT;

-- DOWN
ALTER TABLE users DROP COLUMN full_name;

5.4 跨 ORM 支持

database-migrations Skill 涵盖多种 Migration 工具:

工具 语言/框架 特点
Prisma Migrate Node.js Schema-first,自动生成 SQL
Drizzle Kit Node.js 轻量,支持 push 和 generate
Django Migrations Python 自动检测模型变更
TypeORM Node.js 装饰器驱动
golang-migrate Go 纯 SQL 文件,简洁
Flyway Java 版本化 SQL 脚本
Alembic Python SQLAlchemy 配套

5.5 危险操作清单

操作 危险等级 安全替代
DROP TABLE 极高 先重命名,观察一周再删
DROP COLUMN 五步法
RENAME COLUMN 五步法
ADD NOT NULL 先加列(可空) → 填数据 → 加约束
ADD INDEX CREATE INDEX CONCURRENTLY
CHANGE TYPE 加新列 → 迁数据 → 删旧列

六、ORM 专用 Skill

6.1 jpa-patterns(Java/Kotlin)

jpa-patterns Skill 聚焦 JPA/Hibernate 的常见陷阱:

N+1 查询问题

java 复制代码
// N+1 问题 --- 1 次查询用户 + N 次查询订单
List<User> users = userRepository.findAll();
for (User user : users) {
    List<Order> orders = user.getOrders();  // 每次触发一条 SQL
}

// 解决方案 1:Fetch Join
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();

// 解决方案 2:EntityGraph
@EntityGraph(attributePaths = {"orders"})
List<User> findAll();

实体关系设计

关系类型 默认加载 推荐加载 原因
@OneToOne EAGER LAZY 避免不必要的 JOIN
@ManyToOne EAGER LAZY 避免级联加载
@OneToMany LAZY LAZY 保持默认
@ManyToMany LAZY LAZY 保持默认

6.2 kotlin-exposed-patterns

Exposed 是 Kotlin 的轻量 ORM,kotlin-exposed-patterns Skill 覆盖:

kotlin 复制代码
// DSL 查询风格
object Users : Table() {
    val id = uuid("id").autoGenerate()
    val name = varchar("name", 255)
    val email = varchar("email", 255).uniqueIndex()
    override val primaryKey = PrimaryKey(id)
}

// 类型安全的查询
transaction {
    Users.select { Users.email eq "user@example.com" }
        .map { row -> User(row[Users.id], row[Users.name]) }
}

HikariCP 连接池配置

kotlin 复制代码
Database.connect(
    HikariDataSource(HikariConfig().apply {
        jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
        maximumPoolSize = 10
        minimumIdle = 2
        idleTimeout = 600000
        connectionTimeout = 30000
    })
)

七、database-reviewer Agent

7.1 Agent 职责

database-reviewer 是 ECC 中专门审查数据库相关变更的 Agent:

复制代码
database-reviewer 审查内容:

✓ Schema 变更安全性(是否需要零停机流程)
✓ 索引使用合理性(是否缺失关键索引)
✓ 查询性能(是否存在全表扫描)
✓ N+1 查询检测
✓ 迁移文件是否有回滚脚本
✓ 数据类型选择是否合理

7.2 触发场景

场景 自动触发
修改了 migration 文件
修改了 ORM 模型
SQL 查询变更
Schema 设计讨论 手动触发

7.3 与其他 Agent 的协作

css 复制代码
database-reviewer
  ↕ 协作
code-reviewer        --- 审查 Repository 层代码
security-reviewer    --- 审查 RLS 策略和权限
build-error-resolver --- 修复 migration 失败

八、实战:零停机添加索引

8.1 场景

你的 orders 表有 1000 万行,需要为 created_at 列添加索引。

8.2 危险做法

sql 复制代码
-- 这会锁表!在 1000 万行上可能需要几分钟
-- 期间所有对 orders 表的写入都会被阻塞
CREATE INDEX idx_orders_created_at ON orders(created_at);

8.3 安全做法

sql 复制代码
-- CONCURRENTLY 不会锁表,但需要更长时间
-- 期间正常的读写操作不受影响
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);

8.4 注意事项

sql 复制代码
CONCURRENTLY 的限制:
1. 不能在事务块中使用
2. 如果中途失败,会留下一个 INVALID 索引
3. 需要额外的磁盘空间(构建期间)
4. 比普通 CREATE INDEX 慢 2-3 倍

失败后的清理:
  -- 检查是否有无效索引
  SELECT indexrelid::regclass, indisvalid
  FROM pg_index WHERE NOT indisvalid;

  -- 删除无效索引,重新创建
  DROP INDEX CONCURRENTLY idx_orders_created_at;
  CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);

九、本课练习

练习 1:查看数据库 Skill(10 分钟)

bash 复制代码
ls skills/postgres-patterns/
ls skills/database-migrations/

回答问题:

  • postgres-patterns 中关于索引的章节涵盖了哪些索引类型?
  • database-migrations 支持哪些 Migration 工具?

练习 2:设计零停机添加索引方案(15 分钟)

这是本课最重要的练习。

场景:你的 users 表有 500 万行,需要为 email 列添加唯一索引。

写出完整的迁移方案:

  1. 迁移 SQL 语句
  2. 可能的失败场景和处理方式
  3. 验证索引创建成功的查询

练习 3:分析 N+1 问题(15 分钟)

写出一段会产生 N+1 查询的代码(用你熟悉的语言和 ORM),然后用两种不同的方式修复它。

练习 4(选做):思考题

在微服务架构中,每个服务有自己的数据库。当一个 Migration 需要跨两个服务的数据库时,零停机五步法还适用吗?需要做哪些调整?


十、本课小结

你应该记住的 内容
数据库 Skill 5 个 Skill 覆盖 OLTP、OLAP、迁移、ORM
零停机核心 每次一件事 + 五步法(加列→双写→迁数据→切读→删旧列)
索引安全 生产环境用 CREATE INDEX CONCURRENTLY
N+1 防护 Fetch Join 或 EntityGraph
database-reviewer 自动审查 Schema 变更、索引、查询性能

十一、下节预告

第 21 课:API 设计 --- RESTful 模式与规范

下节课我们将学习 ECC 的 api-design Skill,掌握资源命名、状态码选择、分页过滤、错误响应等 RESTful API 设计的核心模式。你还将了解不同框架(Django、Spring Boot、NestJS 等)的 API Skill 如何与通用 api-design 协作。

预习建议 :提前浏览 skills/api-design 目录和 rules/common/patterns.md 中的 API Response 格式部分。

相关推荐
王小酱4 小时前
第 24 课:安全(下)— 防御机制与实战
ai编程
王小酱4 小时前
第 10 课:Hooks — 事件驱动自动化
openai·ai编程·aiops
王小酱4 小时前
第 16 课:多代理编排 — 并行、视角与隔离
openai·ai编程
王小酱4 小时前
第 15 课:会话管理 — 上下文、模型与持久化
openai·ai编程·aiops
王小酱4 小时前
第 27 课:Agent 工程与 LLM 成本优化
ai编程
王小酱4 小时前
第 11 课:Scripts — Hook 的底层实现
openai·ai编程·aiops
王小酱4 小时前
第 17 课:后端语言 — Python / Go / Rust / Java
openai·ai编程
王小酱4 小时前
第 22 课:软件架构 — 六边形、微服务与决策记录
ai编程
王小酱4 小时前
第 4 课:Rules(上)— 通用规则体系
openai·ai编程·aiops