Kodokon kodokon.com

数据建模:规范化与反规范化

把你的表结构从 1NF 组织到 3NF 以消除各种异常,并懂得何时在充分知情下进行反规范化。

10 分钟 · 3 题

在 Kodokon 中打开本课

一个糟糕的表结构会让你付出好几年的代价。规范化只追求一件事:每一个事实都只被存储一次,以此消除三种异常:更新异常(要在十个地方改同一个邮箱)、插入异常(没有订单就没法添加一件商品)以及删除异常(删掉最后一条订单就把客户也抹掉了)。来看看这个故意设计得很糟的表结构。

SQL
CREATE TABLE bad_orders (
  id INTEGER PRIMARY KEY,
  customer_name TEXT,
  customer_email TEXT,
  product_names TEXT,
  total REAL
);

INSERT INTO bad_orders
  (customer_name, customer_email,
   product_names, total)
VALUES
  ('Alice', 'alice@mail.com',
   'keyboard,mouse', 74.0),
  ('Alice', 'alice@mail.co',
   'monitor', 199.0);
一列里塞了一个列表,一个邮箱还带着拼写错误被复制了一份:问题全在这儿了。

前三个范式,在实践中怎么理解。1NF:原子值 - product_names = 'keyboard,mouse' 违反了它,使得任何对商品的 WHEREJOIN 都无从下手。2NF(复合主键):每一列都依赖于整个主键 - 在一条由(订单,商品)标识的订单行里存放商品的目录价格,就违反了它,因为价格只依赖于商品。3NF:没有任何非主键列依赖于另一个非主键列 - customer_email 依赖于 customer_name,违反了它,于是就有了 alice@mail.co 这个重复副本。下面是修正后的表结构。

SQL
CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL NOT NULL
);

CREATE TABLE purchases (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL
    REFERENCES customers(id)
);

CREATE TABLE purchase_items (
  purchase_id INTEGER NOT NULL
    REFERENCES purchases(id),
  product_id INTEGER NOT NULL
    REFERENCES products(id),
  quantity INTEGER NOT NULL,
  PRIMARY KEY (purchase_id, product_id)
);
每个事实都只住在一个地方;外键把这一切串联起来。
SQL
PRAGMA foreign_keys = ON;

INSERT INTO customers (name, email)
VALUES ('Alice', 'alice@mail.com');

INSERT INTO products (name, price)
VALUES ('keyboard', 49.0), ('mouse', 25.0);

INSERT INTO purchases (customer_id) VALUES (1);

INSERT INTO purchase_items
  (purchase_id, product_id, quantity)
VALUES (1, 1, 1), (1, 2, 1);

SELECT c.name, p.name AS product,
       i.quantity, p.price
FROM purchase_items AS i
JOIN purchases AS pu ON pu.id = i.purchase_id
JOIN customers AS c ON c.id = pu.customer_id
JOIN products AS p ON p.id = i.product_id;
完整的明细通过连接被重新拼装出来。

那么,什么时候才应该反规范化呢?当读取经过实测成为瓶颈时:某个关键页面上的五表连接、每次渲染都要重算的聚合值。几种正当的模式:一个缓存列(purchases.total 在下单时就被冻结下来 - 而且理由充分:付出的价格是一项历史事实,有别于目录价格)、一个由触发器维护的计数器,或者一张批量重建的报表表。要付出的代价:每一份副本都可能发生漂移,而如今负责保证一致性的是你的代码,而不是引擎。

知识检测

确认你已牢记本课的重点内容。

  1. 存放 'keyboard,mouse' 的 product_names 列违反了哪个范式?
    • 1NF
    • 2NF
    • 3NF
  2. 哪种症状预示着违反了 3NF?
    • 由两列构成的主键
    • 一个依赖于另一个非主键列的非主键列
    • 一张没有二级索引的表
  3. 什么时候反规范化才是合理的?
    • 从设计阶段就开始,以省去将来的连接
    • 在测量之后,针对关键读取,并接受不一致的风险
    • 永远不该:无论如何表结构都必须保持 3NF