Skip to content

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:设计一张"标签"表 ​

要求:一篇文章多个标签、按标签查询文章、标签可改名。设计表结构(文章表、标签表、关联表),并加上必要索引。


相关文档 ​

本区 ​

其他域 ​