44.6 关系模型与范式

一句话

范式解决的是 “同一事实只存一处”:1NF 去重复组、2NF 去部分依赖、3NF 去传递依赖、BCNF 去 “非候选键决定”;但范式不是目标,读写模式才是——分析型负载常常故意反范式,用冗余换掉 JOIN。

四个范式(用一句话说清)

范式 消除什么 反例
1NF 字段里存 “多值/重复组” tags = "a,b,c" 存在一个字段
2NF 非主属性依赖 “联合主键的一部分” (order_id, sku_id) → sku_name(只依赖 sku_id)
3NF 非主属性依赖 “另一个非主属性” order → user_id → user_city(city 传递依赖)
BCNF 任何决定因素不是候选键 一个表里有多个 “候选键交叉决定”

一次拆表:从 “一张大表” 到 3NF

-- 反例:所有信息塞一张表,插入/更新/删除都有异常
CREATE TABLE orders_bad (
  order_id INT, user_id INT, user_name TEXT, user_city TEXT,
  sku_id INT, sku_name TEXT, qty INT, price DECIMAL
);
-- 问题:user_city 随 user 重复;改名要改多行;删掉最后一个订单会丢掉用户信息

-- 3NF 拆分:每个事实一处
CREATE TABLE users   (user_id PK, name, city);
CREATE TABLE orders  (order_id PK, user_id FK, created_at);
CREATE TABLE items   (order_id FK, sku_id FK, qty, price, PRIMARY KEY(order_id, sku_id));
CREATE TABLE skus    (sku_id PK, name);

代价是查询要 JOIN:查 “订单 + 用户名 + 商品名” 从 1 次变 3 次表访问。范式用 JOIN 换一致性。

反范式:什么时候故意冗余

场景 做法 代价
读多写少、报表/列表页 宽表/物化视图,冗余用户名 更新要同步冗余
高频 JOIN 的详情页 订单表冗余 buyer_name 改名要异步刷新
数仓/OLAP 星型/雪花模型,维度冗余 存储变大
跨库查询 冗余快照字段(见 32.6 共享数据库反模式) 一致性靠事件/对账

判据:“这个冗余字段的更新频率 × 一致性要求” 是否可接受。可接受就冗余,不可接受就走 JOIN/API。

反模式

反模式 后果
字段里存逗号分隔的多值 无法索引、无法约束、查询要 LIKE
为了 “少 JOIN” 把所有表合成一张 更新异常、冗余失控
只到 1NF 就上线 部分依赖导致 “改一处漏一处”
无脑 3NF 到极致 查询要 6 表 JOIN,性能崩
反范式但不做同步 冗余字段永久不一致
用 “范式” 当教条拒绝冗余 报表查询拖垮主库

三条铁律

🔑 范式保证 “一处存一份”,反范式换取 “一次查一条”。

🔑 反范式必须配同步机制(事件/CDC),否则就是脏数据。

🔑 OLTP 偏范式,OLAP 偏反范式;不要用一套标准套所有场景。

自检

Q1 1NF 为什么要求 “字段不可再分”?

▸ 答案:因为关系模型的操作(选择、投影、连接)都以 “列是原子值” 为前提:tags="a,b,c" 无法建索引、无法用 WHERE tag='a' 精确匹配、无法做外键约束。要么拆成关联表,要么用数组类型 + 专门的索引(如 GIN),但后者已不是 “纯关系” 用法。

Q2 2NF 与 3NF 的区别用一个例子说明?

▸ 答案:(order_id, sku_id) → qty, sku_name:sku_name 只依赖 sku_id(联合主键的一部分)→ 违反 2NF;order_id → user_id → user_city:user_city 依赖非主属性 user_id(传递依赖)→ 违反 3NF。前者拆出 sku 表,后者拆出 user 表。

Q3 为什么 3NF 的表 “更新更安全”?

▸ 答案:因为每个事实只有一处:改用户城市只改 users 一行,不会出现 “订单表里改了、用户表里没改” 的不一致。范式消除的是更新异常/插入异常/删除异常,不是 “为了好看”。

Q4 反范式一定会导致不一致吗?

▸ 答案:不一定,但要付代价:要么同事务双写(写放大、跨表事务),要么异步同步 + 对账(短暂不一致)。没有同步机制的反范式才是 “必然不一致”。

Q5 星型模型为什么在数仓里流行?

▸ 答案:因为它把 “分析查询常用的维度” 冗余到维表,事实表只存 “外键 + 度量”:JOIN 次数少、列少、适合列存与聚合。它本质是 “为读路径反范式”,代价是维度更新要同步到维表。

Q6 什么时候应该从 3NF 走向反范式?

▸ 答案:当出现 “高频查询需要多表 JOIN、且写频率低” 时:报表、列表页、详情页。判断依据是 “JOIN 的成本 × 频率” 是否超过 “冗余的存储 + 同步成本”,而不是 “范式好不好”。

Q7 分库分表后,范式还重要吗?

▸ 答案:更重要,但形式变了:跨库 JOIN 不可用,所以要么 “按查询边界冗余”,要么 “读侧宽表”(见 32.1 CQRS)。此时 “每个库内部仍要范式”,跨库用冗余/读模型替代 JOIN。

关联