04 · Schema 设计与范式 / 反范式
表怎么建决定后面十年的改动成本。本篇讲范式在防什么(冗余与更新异常)、反范式在省什么(一次 JOIN),以及一个前端最容易忽略的事实:建表决定查询能不能快。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 从"能存"到"好存好查":范式消除冗余,反范式换取性能 |
| 核心内容 | 三范式各防什么、何时刻意反范式、字段类型与命名约定、迁移心态 |
| 读完能回答 | 为什么要有 user_id 外键而不是把用户名塞进订单表?反范式什么时候值得? |
| 相关板块 | DB01 关系模型 | DB02 索引 | C10 数据建模与数据访问 |
一、范式:消除"一处数据多个副本"
| 范式 | 一句话 | 防什么 |
|---|---|---|
| 1NF | 每列原子,不存数组/JSON 当"假多值" | 拆分困难、查询要拆字符串 |
| 2NF | 非主键列完全依赖主键(不部分依赖) | 同一行塞了不属于它的东西 |
| 3NF | 非主键列不传递依赖其他非主键列 | 改一处要改多处 |
3NF 的例子
text
❌ orders(id, user_id, user_name, user_phone)
user_name/user_phone 由 user_id 决定 → 传递依赖
用户改手机号,所有历史订单都要跟着改
✅ orders(id, user_id)
users(id, name, phone)
用户信息只存一处,订单只存 user_id范式的真正收益:单一事实来源,杜绝"改了一处忘了另一处"的更新异常。这是它最重要的价值,不只是"显得规范"。
二、反范式:什么时候故意"重复存"
| 场景 | 反范式做法 | 代价 |
|---|---|---|
| 订单列表页高频显示用户名 | 在订单表冗余 user_name | 用户改名时要同步更新 |
| 计数器(点赞数/浏览数) | 冗余一个 count 字段 | 要维护一致性(事务/事件) |
| 报表/宽表 | 物化汇总宽表 | 刷新时机与延迟 |
判断框架:写一致性成本 < 读性能收益时才反范式。 关键问题:"冗余字段改了,我能保证同步更新吗?" 不能就回退到 JOIN。
| 范式化 | 反范式化 |
|---|---|
| 写简单、一致性强 | 读快、少 JOIN |
| 读要 JOIN | 写复杂、要同步 |
| 默认选择 | 性能瓶颈出现后再用 |
三、建表时最容易犯的错
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 主键用业务字段(手机号/身份证) | 改不了、容易被遍历 | 自增/雪花代理主键,业务字段做唯一索引 |
用 text 存 JSON 当"灵活" | 无法索引、无法约束 | 真有 JSON 需求用数据库原生 JSON 类型 |
| 所有字段都可空 | 查询条件含糊、统计不准 | 有默认值的给默认值 |
| 表名/字段名大小写混用 | 跨库迁移噩梦 | 统一小写下划线(user_id) |
| 时间存成字符串 | 排序/比较失效 | 用 datetime/timestamptz,并统一时区 |
四、外键与软删除:两个容易被忽略的决策
4.1 外键要不要
| 用外键 | 不用外键 | |
|---|---|---|
| 一致性 | 数据库保证 | 应用保证 |
| 性能 | 删除/插入要检查,略慢 | 略快 |
| 适用 | 中小系统、强一致 | 高频写、分库分表后 |
建议:单库、低频写时用外键兜底;分库分表、超高频写时放弃外键,靠应用层一致性 + 定期对账。
4.2 软删除
用 deleted_at 而非物理删除:可恢复、保留审计。 代价:所有查询都要带 WHERE deleted_at IS NULL,且唯一索引要设计成"带删除标记的联合唯一"。
五、表设计的两步走
text
第一步:从业务实体建模(用户、订单、商品…)
第二步:从查询反推(高频查询要什么形状?要不要冗余/宽表?)常见错误:只按"实体"建模,从不看查询。建表前先列Top 5 高频查询,让表结构和索引服务于它们。
六、自检清单
- [ ] 能说出 3NF 防什么(冗余与更新异常)
- [ ] 知道反范式的前提是"同步成本可控"
- [ ] 主键用代理键,业务字段做唯一索引
- [ ] 字段类型选择正确(时间用时间类型、不用字符串)
- [ ] 命名统一小写下划线
- [ ] 建表前先列高频查询,让模型服务于查询
- [ ] 外键与软删除的选择是有意识做出的,而非默认
七、常见误区
| 误区 | 事实 |
|---|---|
| "范式越高越好" | 高范式读要 JOIN,性能瓶颈时要反范式 |
| "反范式是坏味道" | 读多写少场景下反范式是正经优化 |
| "手机号可以当主键" | 业务字段会变、可遍历,用代理键 |
| "JSON 字段最灵活" | 无法约束、难以索引,能拆就拆 |
| "软删除零成本" | 所有查询要带条件,索引要专门设计 |
| "外键一定要加/一定不加" | 取决于一致性与写频率 |
八、动手练习
练习 A:规范化一个坏表
表 orders(id, user_name, user_phone, product_name, product_price, tags),把它拆成满足 3NF 的几张表,并说明每一处消除了什么异常。
练习 B:判断该不该反范式
场景:订单列表页要显示用户名,订单每秒写入 1000 条、用户改名极少。你会冗余 user_name 吗?写出你的判断依据。
练习 C:设计一张"标签"表
要求:一篇文章多个标签、按标签查询文章、标签可改名。设计表结构(文章表、标签表、关联表),并加上必要索引。
相关文档
本区
- 01 · 关系模型与 SQL 基础 —— 建模之前先有集合思维
- 02 · 索引原理与优化 —— 表结构决定索引怎么建
- 05 · Redis 与缓存模式 —— 反范式的另一种形态是缓存
其他域
- 10 · 数据建模与数据访问 —— ORM 里的模型映射
- 13 · 缓存与 Redis —— 冗余与缓存的一致性