所属阶段:第四阶段「语言与框架」(第 17-22 课) 前置条件:第 17 课(后端语言) 本课收获:体验一次 Migration 辅助,理解零停机迁移流程
一、本课概述
数据库是大多数应用的核心。一个糟糕的 Schema 设计会让整个系统变慢,一次不安全的 Migration 会让生产环境宕机。ECC 提供了从 Schema 设计到查询优化到零停机迁移的完整 Skill 支持。
本课回答三个问题:
- ECC 有哪些数据库 Skill? --- 覆盖 OLTP 和 OLAP 场景
- 零停机迁移怎么做? --- 五步安全迁移法
- 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 强调以下设计原则:
选择正确的数据类型:
| 需求 | 推荐类型 | 不推荐 | 原因 |
|---|---|---|---|
| 主键 | UUID 或 BIGSERIAL |
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 列添加唯一索引。
写出完整的迁移方案:
- 迁移 SQL 语句
- 可能的失败场景和处理方式
- 验证索引创建成功的查询
练习 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 格式部分。