module5
60 分钟

数据库设计与优化

学习进度

当前模块0/6 课
总进度0/36 课
学习时长0 分钟

学习目标

  • 用 Codex 设计高性能数据库 Schema
  • 生成和审核迁移脚本
  • 识别和优化慢查询
  • 制定备份与恢复策略
  • 是否包含 `DROP TABLE` 或 `DROP COLUMN`(检查是否误删)
  • 新增字段是否有默认值(避免生产报错)
  • 大表是否使用了 `CONCURRENTLY`(避免锁表)
  • 是否有对应的回滚方案
  • 数据迁移逻辑是否正确

404 数据库设计与优化

AI 辅助数据库 Schema 设计、索引优化、查询调优与备份策略

学习目标

完成本课程后,你将能够:

一、Schema 设计原则

用 Codex 审查 Schema 设计

/review 请审查我的 Prisma Schema,检查以下问题:
1. 是否存在 N+1 查询风险
2. 索引设计是否合理
3. 字段类型选择是否正确
4. 是否存在数据冗余
5. JSON 字段是否应该拆表

常见 Schema 问题与修复

问题示例修复方案
缺失索引按 status 查询无索引添加 `@@index([status])`
JSON 滥用标签存为 JSON 数组拆为独立 Tag 表 + 关联表
文本字段过大所有 text 用 TEXT 类型短字符串用 VARCHAR(n)
缺失级联删除删项目后任务残留添加 `onDelete: Cascade`
时序数据未分区日志表无限增长按月份分区表

索引设计指南

// ✅ 好的索引设计
model Task {
  id        String   @id @default(cuid())
  title     String
  status    TaskStatus
  priority  Priority
  dueDate   DateTime?
  projectId String
  assigneeId String?
  createdAt DateTime @default(now())

  // 复合索引:覆盖最常见的查询组合
  @@index([projectId, status])       // 按项目 + 状态筛选
  @@index([assigneeId, status])      // 我的任务筛选
  @@index([status, priority])         // 优先级排序
  @@index([dueDate])                  // 截止日期排序(B-tree 索引)
  @@index([createdAt])               // 时间范围查询
}

二、迁移脚本管理

生成迁移

Prisma 迁移命令速查:

npx prisma migrate dev --name add_tags_table
  → 开发环境:生成并应用迁移

npx prisma migrate dev --create-only
  → 只生成迁移文件,手动审核后执行

npx prisma migrate deploy
  → 生产环境:执行所有待处理迁移

npx prisma migrate status
  → 查看迁移状态

迁移脚本审核清单

Codex 生成的迁移脚本上线前必须检查:

数据迁移示例

请为 Task 表生成数据迁移脚本:
- 将现有的 status 字段从 VARCHAR 改为 ENUM 类型
- 迁移期间保持服务可用
- 包含回滚脚本

步骤:
1. 新增临时 ENUM 列
2. 数据批量迁移(分批 1000 条)
3. 切换读写列
4. 删除旧列

三、查询优化

用 Codex 分析慢查询

/debug 以下 API 响应时间超过 2 秒,请分析原因并给出优化方案:

GET /api/tasks?projectId=xxx&page=1&limit=20

Prisma 查询:
const tasks = await prisma.task.findMany({
  where: { projectId },
  include: {
    assignee: true,
    tags: { include: { tag: true } },
    comments: { include: { author: true } },
  },
  orderBy: { createdAt: 'desc' },
  take: 20,
  skip: 0,
});

典型优化方案

**问题 1:N+1 查询**

使用 include 已经避免了 N+1,但嵌套 include 仍可能产生多余的 JOIN。
优化:对非必要字段使用 select 替代 include

**问题 2:返回数据过多**

// ❌ 返回所有字段(包括大文本)
include: { assignee: true }

// ✅ 只选择需要的字段
const tasks = await prisma.task.findMany({
  where: { projectId },
  select: {
    id: true,
    title: true,
    status: true,
    priority: true,
    dueDate: true,
    assignee: {
      select: { id: true, name: true, avatar: true },
    },
    _count: {
      select: { comments: true },
    },
  },
  orderBy: { createdAt: 'desc' },
  take: 20,
  skip: 0,
});

**问题 3:缺失分页游标**

// 基于游标的分页(比 offset 更高效)
const tasks = await prisma.task.findMany({
  where: { projectId },
  take: 20,
  cursor: cursor ? { id: cursor } : undefined,
  skip: cursor ? 1 : 0, // 跳过游标本身
  orderBy: { createdAt: 'desc' },
});

查询优化效果对比

优化项优化前优化后提升
添加索引800ms45ms94%
select 替代 include350ms80ms77%
游标分页200ms30ms85%
Redis 缓存热数据120ms5ms96%

四、缓存策略

多级缓存架构

请求 → Redis 缓存(热数据)
        ↓ 未命中
      API 服务(应用层缓存)
        ↓ 未命中
      PostgreSQL(数据库层)

Redis 缓存实现

// backend/src/services/cache.ts
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

export class CacheService {
  static async getOrSet<T>(
    key: string,
    fetchFn: () => Promise<T>,
    ttl = 300 // 5分钟默认
  ): Promise<T> {
    const cached = await redis.get(key);
    if (cached) return JSON.parse(cached);

    const data = await fetchFn();
    await redis.setex(key, ttl, JSON.stringify(data));
    return data;
  }

  static async invalidate(pattern: string): Promise<void> {
    const keys = await redis.keys(pattern);
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }
}

// 使用示例
const tasks = await CacheService.getOrSet(
  `tasks:board:${userId}`,
  () => prisma.task.findMany({ where: { assigneeId: userId } }),
  60 // 1分钟
);

// 创建/更新任务后失效缓存
await CacheService.invalidate('tasks:*');

五、备份与恢复策略

用 Codex 生成备份方案

请为 PostgreSQL 生成自动化备份方案:
- 每日全量备份(凌晨 3 点)
- 每 6 小时增量备份(WAL 归档)
- 保留最近 7 天全量 + 30 天增量
- 备份文件加密存储到 S3
- 包含恢复验证脚本

备份脚本

#!/bin/bash
# backup.sh — PostgreSQL 自动备份

DB_NAME="taskflow"
BACKUP_DIR="/backups/postgres"
S3_BUCKET="s3://taskflow-backups"
RETENTION_DAYS=7

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"

# 执行备份
pg_dump -Fc -d "$DB_NAME" -f "$BACKUP_FILE"

# 加密
gpg --encrypt --recipient backup@company.com "$BACKUP_FILE"

# 上传到 S3
aws s3 cp "${BACKUP_FILE}.gpg" "${S3_BUCKET}/daily/"

# 清理过期备份
find "$BACKUP_DIR" -name "*.dump*" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: ${BACKUP_FILE}.gpg"

恢复验证

备份策略检查清单:
- [ ] 最近一次全量备份是否成功
- [ ] WAL 归档是否持续写入
- [ ] 恢复流程是否通过演练
- [ ] RPO(恢复点目标)是否达标
- [ ] RTO(恢复时间目标)是否达标

六、本课总结

**核心要点:**

  • Schema 设计后让 Codex 审查,提前发现 N+1、索引缺失等问题
  • 迁移脚本上线前必须人工审核,特别是 DROP 和 ALTER 操作
  • 查询优化三板斧:添加索引、select 替代 include、游标分页
  • Redis 缓存热数据,配合 Cache-Aside 模式管理失效
  • 备份不仅要执行,还要定期验证恢复流程

**下一步:** 模块 405 — 部署与运维

下一模块