MySQL 三大范式与反范式
·
我刚工作的时候,设计了一张用户表,把所有信息都塞进去了:用户 ID、姓名、年龄、邮箱、地址、订单 ID、订单金额、订单状态……结果表有 50 多个字段,查询慢得要命,还经常冗余数据不一致。
后来 DBA 帮我重新设计表结构,遵循了数据库三大范式,性能直接提升了 10 倍。
今天咱们就来聊聊 MySQL 的三大范式与反范式,看完这篇,你就能设计出高性能的表结构了。
为什么要有范式?
范式(Normal Form,NF) 是数据库设计的一套规范,目的是:
- 减少数据冗余:同样的数据不存多份
-
- 避免数据异常:插入异常、删除异常、更新异常
-
- 提高数据一致性:数据只存一份,不会出现不一致
但是,过度遵循范式会导致表太多、JOIN 太多,性能反而下降。所以有时候要反范式(故意冗余数据)。
- 提高数据一致性:数据只存一份,不会出现不一致
第一范式(1NF):列不可再分
定义:表中的每一列都是原子性的(不可再分)。
违反 1NF 的例子
-- 违反 1NF:phone 列存了多个值
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
phone VARCHAR(100) -- 存了 "13800138000,13900139000"
);
```
**问题**:如果要查 "13800138000" 这个手机号的用户,很难写 SQL。
### 符合 1NF 的设计
```sql
-- 符合 1NF:拆成多行
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE user_phones (
user_id INT,
phone VARCHAR(20),
PRIMARY KEY (user_id, phone)
);
```
**或者**(如果只需要 2 个手机号):
```sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
phone1 VARCHAR(20),
phone2 VARCHAR(20)
);
```
## 第二范式(2NF):消除部分依赖
**定义**:表中的每一列都**完全依赖**于主键(不能是部分依赖)。
**前提**:表必须有**联合主键**(如果主键是单字段,自动满足 2NF)。
### 违反 2NF 的例子
```sql
-- 违反 2NF:联合主键 (order_id, product_id),但 product_name 只依赖于 product_id(部分依赖)
CREATE TABLE order_items (
order_id INT,
product_id INT,
product_name VARCHAR(50), -- 只依赖于 product_id,不依赖于 order_id
quantity INT,
PRIMARY KEY (order_id, product_id)
);
```
**问题**:
1. **数据冗余**:同一个 `product_id` 的 `product_name` 存了多次
2. 2. **更新异常**:如果 `product_name` 改了,要更新多行
3. 3. **插入异常**:如果还没订单,就没法插产品信息
### 符合 2NF 的设计
```sql
-- 拆成两张表
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(50)
);
```
## 第三范式(3NF):消除传递依赖
**定义**:表中的每一列都**直接依赖**于主键(不能是传递依赖)。
### 违反 3NF 的例子
```sql
-- 违反 3NF:department_name 依赖于 department_id,department_id 依赖于 emp_id(传递依赖)
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(50),
department_id INT,
department_name VARCHAR(50) -- 传递依赖:emp_id → department_id → department_name
);
```
**问题**:
1. **数据冗余**:同一个部门的员工,部门名存了多次
2. 2. **更新异常**:如果部门名改了,要更新多行
3. 3. **插入异常**:如果还没员工,就没法插部门信息
### 符合 3NF 的设计
```sql
-- 拆成两张表
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(50),
department_id INT
);
CREATE TABLE departments (
department_id INT PRIMARY KEY,
department_name VARCHAR(50)
);
```
## BC 范式(BCNF):消除主属性对候选键的部分依赖
**定义**:如果表有**多个候选键**,不能有主属性对候选键的部分依赖。
### 违反 BCNF 的例子
```sql
-- 假设:(student_id, course_id) 是联合主键,(professor_id, course_id) 也是候选键
CREATE TABLE enrollments (
student_id INT,
course_id INT,
professor_id INT,
PRIMARY KEY (student_id, course_id)
);
```
**问题**:`professor_id` 依赖于 `course_id`(部分依赖候选键),导致数据冗余。
### 符合 BCNF 的设计
```sql
-- 拆成两张表
CREATE TABLE enrollments (
student_id INT,
course_id INT,
PRIMARY KEY (student_id, course_id)
);
CREATE TABLE courses (
course_id INT PRIMARY KEY,
professor_id INT
);
```
## 第四范式(4NF)和第五范式(5NF)
这两范式很少用到,简单提一下:
- **4NF**:消除多值依赖(比如一个表同时存了"课程"和"爱好"两个多值属性)
- - **5NF**:消除连接依赖(确保表是不可再分的)
**实际开发中,一般遵循到 3NF 就够了。**
## 反范式(Denormalization):故意冗余数据
**反范式**指的是:**故意违反范式,冗余一些数据**,目的是**减少 JOIN,提升查询性能**。
### 什么时候要反范式?
1. **读多写少**的场景(比如电商的订单表、商品表)
2. 2. **JOIN 太多**,导致查询性能差
3. 3. **实时性要求高**,不能每次都 JOIN
### 反范式的例子
#### 例子 1:订单表冗余用户名
**遵循 3NF 的设计**:
```sql
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
created_at DATETIME
);
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50)
);
-- 查询订单时,要 JOIN users 表
SELECT o.order_id, u.username, o.amount
FROM orders o
JOIN users u ON o.user_id = u.user_id;
反范式设计(冗余 username):
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
username VARCHAR(50), -- 冗余字段
amount DECIMAL(10,2),
created_at DATETIME
);
-- 查询订单时,不需要 JOIN
SELECT order_id, username, amount FROM orders;
好处:查询快了(不需要 JOIN)。
代价:
- 如果用户改了用户名,要更新所有相关订单(可以用触发器或者定时任务)
-
- 冗余字段占存储空间
例子 2:商品表冗余分类名
遵循 3NF 的设计:
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(50),
category_id INT
);
CREATE TABLE categories (
category_id INT PRIMARY KEY,
category_name VARCHAR(50)
);
-- 查询商品时,要 JOIN categories 表
SELECT p.product_name, c.category_name
FROM products p
JOIN categories c ON p.category_id = c.category_id;
反范式设计(冗余 category_name):
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(50),
category_id INT,
category_name VARCHAR(50) -- 冗余字段
);
-- 查询商品时,不需要 JOIN
SELECT product_name, category_name FROM products;
实战建议
1. 先遵循 3NF,再按需反范式
建议流程:
- 先按 3NF 设计表结构(减少冗余,保证一致性)
-
- 上线后,如果发现某些查询性能差(JOIN 太多),再考虑反范式(冗余字段)
2. 反范式时,要考虑数据同步问题
如果冗余字段会更新,要设计好同步机制:
- 方案 1:用触发器(自动同步)
-
- – 当 users 表的 username 更新时,自动更新 orders 表的 username
- DELIMITER $$
- CREATE TRIGGER update_username AFTER UPDATE ON users
- FOR EACH ROW
- BEGIN
-
UPDATE orders SET username = NEW.username WHERE user_id = NEW.user_id; - END$$
- DELIMITER ;
-
- 方案 2:用定时任务(定期同步)
-
- – 每天凌晨同步用户名
- UPDATE orders o
- JOIN users u ON o.user_id = u.user_id
- SET o.username = u.username
- WHERE o.username != u.username;
-
- 方案 3:不更新(允许短期不一致,比如订单的用户名,下完单就不应该改了)
3. 用「宽表」做报表查询
宽表指的是:把多张表的数据冗余到一张表,专门用来做报表查询(不需要 JOIN)。
-- 宽表:订单明细表(冗余了用户信息、商品信息、分类信息)
CREATE TABLE order_details (
order_id INT,
user_id INT,
username VARCHAR(50),
product_id INT,
product_name VARCHAR(50),
category_name VARCHAR(50),
amount DECIMAL(10,2),
created_at DATETIME
);
```
**好处**:报表查询超快(不需要 JOIN)。
**代价**:数据冗余,同步复杂。
## 总结
- **第一范式(1NF)**:列不可再分(原子性)
- - **第二范式(2NF)**:消除部分依赖(联合主键时,每一列都完全依赖于主键)
- - **第三范式(3NF)**:消除传递依赖(每一列都直接依赖于主键)
- - **BC 范式(BCNF)**:消除主属性对候选键的部分依赖
- - **反范式**:故意冗余数据,减少 JOIN,提升查询性能
- - 实战建议:先遵循 3NF,再按需反范式;反范式时,要考虑数据同步问题;用「宽表」做报表查询
如果你能把三大范式和反范式讲清楚,并且能设计合理的表结构,面试官绝对觉得你是高级开发。
---
**实战代码都在我本地跑过,你可以放心复制。** 如果有问题,欢迎评论区交流!
更多推荐



所有评论(0)