Chapter 09

数据库设计:范式与 ER 图

1NF/2NF/3NF/BCNF 规范化理论、ER 图设计、实战电商数据库

9.1 为什么需要规范化

不良的表设计会导致数据冗余(同一数据在多处存储),进而引发三种"更新异常":

反面示例:不规范的 orders 表

order_idcustomer_namecustomer_emailproduct_namecategoryprice
1Alicealice@ex.com键盘电脑外设599
2Alicealice@ex.com鼠标电脑外设129
3Bobbob@ex.com键盘电脑外设599

解决方案:把客户信息、商品信息、订单信息分别放到独立的表中。这就是规范化(Normalization)。

9.2 ER 图(实体关系图)

ER 图(Entity-Relationship Diagram)是数据库设计的"蓝图",在写 SQL 之前用图形表达数据模型。

📊

电商系统 ER 图(文字描述):
User(id, username, email)
Product(id, name, price, category_id)
Category(id, name)
Order(id, user_id, total_price, status)
OrderItem(id, order_id, product_id, quantity, price)
Address(id, user_id, street, city, province)

关系:User 1:N Order,Order 1:N OrderItem,
Product 1:N OrderItem,User 1:N Address,
Category 1:N Product(Product 与 Order 是 M:N,通过 OrderItem 实现)

9.3 函数依赖

9.4 第一范式(1NF)

1NF 要求:表中每一列都是原子值(不可再分),不允许多值属性或重复组。

违反 1NF 的例子

student_idnamephones(多值)
1Alice13812345678, 02188888888

改为 1NF

把多值属性拆分到单独的行,或创建独立的 phones 表:

SQLCREATE TABLE student_phones (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  student_id INT NOT NULL,
  phone      VARCHAR(20) NOT NULL,
  phone_type ENUM('mobile', 'home', 'work') DEFAULT 'mobile'
);

9.5 第二范式(2NF)

2NF 要求:满足 1NF,且每个非主键属性完全依赖于主键(消除部分函数依赖)。2NF 仅在复合主键时才需要关注。

违反 2NF 的例子(订单明细表)

(order_id, product_id) 复合主键quantityproduct_name(只依赖 product_id)product_price(只依赖 product_id)
(1, 101)2键盘599
(1, 102)1鼠标129
(2, 101)1键盘599(重复!)

改为 2NF

SQL-- 把依赖 product_id 的属性拆出去
CREATE TABLE products (
  id    INT PRIMARY KEY,
  name  VARCHAR(100),
  price DECIMAL(10,2)
);

CREATE TABLE order_items (
  order_id   INT,
  product_id INT,
  quantity   INT NOT NULL,
  -- 保存下单时的快照价格(允许冗余,因为价格可变)
  unit_price DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (order_id, product_id),
  FOREIGN KEY (product_id) REFERENCES products(id)
);

9.6 第三范式(3NF)

3NF 要求:满足 2NF,且每个非主键属性直接依赖于主键,不存在传递依赖。

违反 3NF 的例子

employee_id (PK)namedept_iddept_name(传递依赖:employee → dept_id → dept_name)
1AliceD01研发部
2BobD01研发部(冗余!)
3CarolD02市场部

改为 3NF

SQL-- 把传递依赖的属性拆到单独的表
CREATE TABLE departments (
  id   CHAR(3) PRIMARY KEY,
  name VARCHAR(50) NOT NULL
);

CREATE TABLE employees (
  id      INT PRIMARY KEY,
  name    VARCHAR(50),
  dept_id CHAR(3) REFERENCES departments(id)
  -- dept_name 不在这里,需要时 JOIN departments 表
);

9.7 BCNF(Boyce-Codd 范式)

BCNF 比 3NF 更严格:对于每一个函数依赖 X → Y,X 必须是超键(能唯一标识整行的列集合)

BCNF 主要解决 3NF 中仍存在的一种特殊情况:非主键属性决定了候选键的一部分。在实际业务中遇到较少,大多数表达到 3NF 即可。

9.8 反范式化(Denormalization)

规范化减少了冗余,但也增加了 JOIN 操作。在读多写少的高并发场景下,可以故意引入冗余来换取查询性能,这叫反范式化。

⚠️

反范式化的代价 冗余字段需要在数据更新时同步维护(多写一份),增加代码复杂度,可能出现不一致。只有在有明确性能瓶颈的前提下才引入反范式化,不要过早优化。

SQL-- 反范式化示例:在 posts 表中冗余存储 comment_count
-- 规范化做法:每次需要评论数时 SELECT COUNT(*) FROM comments WHERE post_id = ?
-- 反范式化:直接在 posts.comment_count 存储冗余值

ALTER TABLE posts ADD COLUMN comment_count INT NOT NULL DEFAULT 0;

-- 插入评论时同步更新 comment_count(通过触发器自动维护)
CREATE TRIGGER trig_inc_comment_count
AFTER INSERT ON comments
FOR EACH ROW
  UPDATE posts SET comment_count = comment_count + 1
  WHERE id = NEW.post_id;

9.9 多对多关系的实现

关系型数据库不能直接表示 M:N 关系,必须通过中间表(联结表/关联表)实现。

SQL-- 示例:文章 与 标签 是 M:N 关系(一篇文章可有多个标签,一个标签可属于多篇文章)
CREATE TABLE tags (
  id   INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE post_tags (           -- 中间表
  post_id INT NOT NULL,
  tag_id  INT NOT NULL,
  PRIMARY KEY (post_id, tag_id),   -- 复合主键防止重复
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id)  REFERENCES tags(id)  ON DELETE CASCADE
);

-- 查询某篇文章的所有标签
SELECT t.name
FROM tags t
INNER JOIN post_tags pt ON t.id = pt.tag_id
WHERE pt.post_id = 1;

-- 查询某个标签下所有文章
SELECT p.title
FROM posts p
INNER JOIN post_tags pt ON p.id = pt.post_id
INNER JOIN tags t ON pt.tag_id = t.id
WHERE t.name = '数据库';

9.10 实战:完整电商数据库设计

🏗️

设计要点:用户表、商品表(含分类)、订单表、订单明细表、地址表。商品价格在 order_items 中存快照(下单时的价格),不受后续商品调价影响。

SQL-- 用户表
CREATE TABLE users (
  id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username   VARCHAR(50)  NOT NULL UNIQUE,
  email      VARCHAR(255) NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 商品分类表
CREATE TABLE categories (
  id        INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name      VARCHAR(100) NOT NULL,
  parent_id INT UNSIGNED NULL,   -- 支持多级分类(自引用)
  FOREIGN KEY (parent_id) REFERENCES categories(id)
);

-- 商品表
CREATE TABLE products (
  id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  category_id INT UNSIGNED NOT NULL,
  name        VARCHAR(200) NOT NULL,
  price       DECIMAL(10,2) NOT NULL CHECK (price >= 0),
  stock       INT UNSIGNED NOT NULL DEFAULT 0,
  is_active   BOOLEAN NOT NULL DEFAULT TRUE,
  FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 收货地址表(用户可有多个地址)
CREATE TABLE addresses (
  id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id    INT UNSIGNED NOT NULL,
  receiver   VARCHAR(50) NOT NULL,
  phone      VARCHAR(20) NOT NULL,
  province   VARCHAR(50) NOT NULL,
  city       VARCHAR(50) NOT NULL,
  street     VARCHAR(200) NOT NULL,
  is_default BOOLEAN NOT NULL DEFAULT FALSE,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 订单表(主表)
CREATE TABLE orders (
  id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id     INT UNSIGNED NOT NULL,
  address_id  INT UNSIGNED NOT NULL,
  total_price DECIMAL(10,2) NOT NULL,
  status      ENUM('pending','paid','shipped','completed','cancelled')
              NOT NULL DEFAULT 'pending',
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id)    REFERENCES users(id),
  FOREIGN KEY (address_id) REFERENCES addresses(id),
  INDEX idx_orders_user_id (user_id)
);

-- 订单明细表(实现 orders 与 products 的多对多)
CREATE TABLE order_items (
  id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id   INT UNSIGNED NOT NULL,
  product_id INT UNSIGNED NOT NULL,
  quantity   INT UNSIGNED NOT NULL DEFAULT 1,
  unit_price DECIMAL(10,2) NOT NULL,    -- 快照价格
  FOREIGN KEY (order_id)   REFERENCES orders(id)   ON DELETE CASCADE,
  FOREIGN KEY (product_id) REFERENCES products(id),
  INDEX idx_items_order_id (order_id)
);

电商 ER 关系汇总

📊

users 1:N addresses(一个用户多个收货地址)
users 1:N orders(一个用户多笔订单)
orders N:1 addresses(每笔订单对应一个地址)
orders 1:N order_items(一笔订单包含多个商品行)
products 1:N order_items(一个商品出现在多笔订单中)
categories 1:N products(一个分类下多个商品)
categories 1:N categories(自引用,支持多级分类)