讲解

表设计的经典理论是范式。第一范式(1NF)要求列是原子的:一个「收货地址」列里塞「省,市,区,详细地址」就违反了 1NF,想按城市统计时只能痛苦地切字符串。第二范式(2NF)要求非主键列完全依赖整个主键,而不是只依赖主键的一部分——这在联合主键的表里才会遇到。第三范式(3NF)要求非主键列不依赖其他非主键列:订单表里存 user_id 就够了,把用户姓名、城市也冗余进来,用户改名时就要同步改成千上万行订单。

用一个反例体会:把订单设计成一张大宽表 orders(id, user_name, user_city, product1, price1, product2, price2, ...),一单一行。它同时违反三个范式:商品列重复且数量有上限(1NF 的原子性延伸)、用户信息依赖 user 而非订单(3NF 传递依赖)。规范的设计是拆成三张表:users(用户)、orders(订单头:属于谁、状态、时间)、order_items(订单明细:哪个订单、什么商品、单价、数量)。订单总额不必存,需要时用 SUM 算出来。

范式不是教条。报表场景为了查询性能做适度反范式(冗余一个总额字段、建汇总表)是常见且合理的工程取舍,前提是明确「以哪份数据为准」并用程序或触发器维护一致性。另外几条实用纪律:主键用无业务含义的自增 BIGINT/INT(别用手机号、身份证号,业务字段会变);每张表带 created_at/updated_at;外键约束在互联网高并发场景常被放弃(改由应用层保证一致性),但学习阶段和内部系统建议保留,它能挡住大量脏数据。

示例

按三范式设计的用户-订单-明细三张表,注意外键把 user_id、order_id 钉死在父表上:

CREATE TABLE users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  city VARCHAR(50)
);

CREATE TABLE orders (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id INT UNSIGNED NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'pending',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id)
);

CREATE TABLE order_items (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id INT UNSIGNED NOT NULL,
  product_name VARCHAR(100) NOT NULL,
  unit_price DECIMAL(10,2) NOT NULL,
  quantity INT NOT NULL DEFAULT 1,
  CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders (id)
);

INSERT INTO users (name, city) VALUES ('李明', '北京'), ('王芳', '上海');
INSERT INTO orders (user_id, status) VALUES (1, 'paid');
INSERT INTO order_items (order_id, product_name, unit_price, quantity) VALUES
  (1, '机械键盘', 299.00, 1),
  (1, '鼠标垫', 29.00, 2);

查询一单的全貌——用户、明细、小计,总额由明细聚合得出而不是单独存储:

SELECT o.id AS order_id, u.name AS user_name, oi.product_name,
       oi.unit_price, oi.quantity, oi.unit_price * oi.quantity AS subtotal
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
JOIN order_items AS oi ON oi.order_id = o.id;

SELECT o.id AS order_id, SUM(oi.unit_price * oi.quantity) AS total
FROM orders AS o JOIN order_items AS oi ON oi.order_id = o.id
GROUP BY o.id;

常见坑

  • 一张大宽表走天下:所有信息塞一张表,加类目不认输就加 column1、column2……后期改结构、加索引都是灾难。先按范式拆,再为性能有意识地冗余。
  • 为了「灵活」上 EAV:实体-属性-值模型(一列属性名、一列属性值)看似永远不用改表,实际查询要写一坨自连接,类型约束全失,是公认的反模式。
  • 拿业务字段当主键:手机号、邮箱都会变,一旦变更所有外键跟着改。主键用无含义的自增 id,业务唯一性用 UNIQUE 约束表达。
  • 订单总额单独存又不维护:存了 total 字段却没有可靠的更新机制,明细改了总额没改,对账时对不上。要么不存现算,要么明确维护责任。

小结

三范式的核心是让每份数据只存一份:原子列、依赖整个主键、无传递依赖;范式之内做设计,性能需要时有意识地反范式。下一章把设计落成 DDL:CREATE、ALTER、DROP 的日常操作。