模块 03第 1 学期 · 基础8 海报条目
数据基础
登录后可在不同设备间同步进度。在此之前,进度只保存在当前浏览器中。
为什么重要
代码可以重新部署,数据坏了却无法复原。数据库是整个系统中你最输不起的部分,偏偏也是 AI 工具对待得最随意的部分。
学习目标
- 从第一天起就用主键、约束和多租户来建模数据。
- 用 EXPLAIN 和索引找出并修复慢查询。
- 消除 N+1 查询。
- 为 serverless 配置连接池。
- 用事务和锁保证并发下的数据正确性。
- 零停机发布 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:把数据层搞坏再修好
- 灌入 100 万行数据。找出最慢的三个查询;用索引修复;展示修复前后的 EXPLAIN。
- 给一个 API 测试加上查询次数断言;找出并修复一个 N+1。
- 写一个测试,并发发起 20 个“兑换优惠券”请求;证明优惠券恰好只被兑换一次。
- 在每张表上启用 RLS;写一个测试,证明用户 A 使用公开的 anon key 读不到用户 B 的行。
- 用 expand-and-contract 方式零停机地重命名一列。
完成标准
AI 提示词包
- “列出这个端点执行的每一个查询,标出任何写在循环里的。”
- “为这些表编写 RLS 策略,让用户只能访问自己所属组织的行。然后写测试尝试绕过它们。”
- “把这次 schema 变更规划成跨多次部署的 expand-and-contract 迁移。”