模块 03第 1 学期 · 基础8 海报条目

数据基础

登录后可在不同设备间同步进度。在此之前,进度只保存在当前浏览器中。

为什么重要

代码可以重新部署,数据坏了却无法复原。数据库是整个系统中你最输不起的部分,偏偏也是 AI 工具对待得最随意的部分。

学习目标

  1. 从第一天起就用主键、约束和多租户来建模数据。
  2. 用 EXPLAIN 和索引找出并修复慢查询。
  3. 消除 N+1 查询。
  4. 为 serverless 配置连接池。
  5. 用事务和锁保证并发下的数据正确性。
  6. 零停机发布 schema 变更。

课程内容

3.1 为正确性建模

  • 让数据库来执行规则:NOT NULL、UNIQUE、CHECK、外键。应用层检查会有竞态,约束不会。
  • 金额:用整数最小单位(分)或 numeric,绝不用浮点数。
  • 时间:存 UTC(timestamptz),展示时再转换。
  • ID:对外 ID 用 UUIDv7 之类;绝不暴露让攻击者能遍历记录的自增 ID。
  • 多租户:每一行都归属于一个 org_id / user_id,每个查询都按它限定范围。在数据库层用 Row-Level Security(Postgres RLS)强制执行,而不只是写在应用代码里。

3.2 索引与查询优化 (25, 26)

  • 索引是一种有序的查找结构;没有索引,数据库就得扫描每一行。
  • 给用于过滤、关联和排序的列建索引。复合索引的列顺序很重要(等值条件列在前,范围条件列在后)。
  • 每个索引都会拖慢写入、占用存储;不要什么都建索引。
  • 读懂 EXPLAIN ANALYZE:留意大表上的顺序扫描、偏高的行数估计、溢出到磁盘的排序。
  • 开启慢查询日志(pg_stat_statements),每周复查。

3.3 N+1 查询 (27)

// N+1: 1 query for orders + 1 per order for its customer
const orders = await db.order.findMany();
for (const o of orders) o.customer = await db.customer.findUnique({ where: { id: o.customerId } });

// Fixed: 2 queries total (or a join)
const orders = await db.order.findMany({ include: { customer: true } });

在测试中借助查询日志来发现:断言每个请求的查询次数。

3.4 连接池 (28)

每个 Postgres 连接都要占用内存;数据库对连接数有上限(通常在 100 左右)。Serverless 函数每次调用都开一个连接,负载一高就会耗尽。在 serverless 场景下,使用 transaction 模式的连接池(PgBouncer、Supavisor、Neon pooler、Prisma Accelerate)。

3.5 事务与锁 (36, 37)

  • 把多步写入包在一个事务里:要么全成功,要么全不做。
  • 乐观锁:加一个 version 列;UPDATE ... SET version = version + 1 WHERE id = $1 AND version = $2;更新了 0 行,说明别人已经改过,此时重试或报错。
  • 悲观锁:SELECT ... FOR UPDATE 在事务内锁住该行。用于余额、库存这类争用激烈的资源。
  • 典型 bug:两次同时点击“批准贷款”,都读到 status = pending,于是放款两次。修复方法:用条件更新(WHERE status = 'pending'),并检查受影响的行数。

3.6 迁移与 schema 版本管理 (81, 82)

  • 每次 schema 变更都是 git 中一个有版本的迁移文件,由 CI 执行,绝不在控制台里手动改。
  • 用 expand and contract(先扩后缩) 实现零停机:(1) 新增可为空的列,(2) 部署同时写新旧两列的代码,(3) 分批回填数据,(4) 切换读取,(5) 在之后的版本里删除旧列。
  • 危险操作:在大表上添加没有默认值的 NOT NULL、重命名正在使用的列、不加 CONCURRENTLY 创建索引。
  • 绝不让 AI agent 直接对生产环境跑迁移。

实验 3:把数据层搞坏再修好

  1. 灌入 100 万行数据。找出最慢的三个查询;用索引修复;展示修复前后的 EXPLAIN。
  2. 给一个 API 测试加上查询次数断言;找出并修复一个 N+1。
  3. 写一个测试,并发发起 20 个“兑换优惠券”请求;证明优惠券恰好只被兑换一次。
  4. 在每张表上启用 RLS;写一个测试,证明用户 A 使用公开的 anon key 读不到用户 B 的行。
  5. 用 expand-and-contract 方式零停机地重命名一列。

完成标准

AI 提示词包

  • “列出这个端点执行的每一个查询,标出任何写在循环里的。”
  • “为这些表编写 RLS 策略,让用户只能访问自己所属组织的行。然后写测试尝试绕过它们。”
  • “把这次 schema 变更规划成跨多次部署的 expand-and-contract 迁移。”