第一范式与第二范式
导学
数据库设计不是"把字段堆进表"那么简单。范式(Normal Form)是关系数据库设计的理论基础,它帮你消除数据冗余、避免更新异常。第一范式和第二范式是最基础的两级台阶,理解了它们,你就不会设计出"一个单元格藏多个值"或"主键的一部分决定非主键列"的糟糕表结构。
第一范式(1NF):原子性
第一范式要求:表的每一列都是不可再分的最小数据单元,每个单元格只包含一个值。
场景一:违反 1NF 的设计
当前数据状态:某系统用一张表记录员工及其联系方式。
CREATE TABLE employees_bad (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20),
phones VARCHAR(100), -- 多个电话用逗号分隔
skills VARCHAR(200) -- 多个技能用逗号分隔
);
INSERT INTO employees_bad VALUES
(1, '大翔', '13800138001,13900139001', 'Java,MySQL,Linux'),
(2, '白歌', '13900139002', 'Python,MySQL');
SELECT * FROM employees_bad;
| emp_id | emp_name | phones | skills |
|---|---|---|---|
| 1 | 大翔 | 13800138001,13900139001 | Java,MySQL,Linux |
| 2 | 白歌 | 13900139002 | Python,MySQL |
问题分析:
phones列存放多个电话号码,用逗号分隔——不是原子值skills列存放多个技能——不是原子值- 想查"会 MySQL 的员工",必须用
LIKE '%MySQL%'——无法利用索引,性能差且结果不准 - 想给大翔增加一个电话,需要读取、拆分、拼接字符串——极易出错
场景二:拆分为 1NF
操作语句:将多值属性拆分为独立的表。
-- 员工基础表(只存原子属性)
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) NOT NULL
);
-- 电话表(一行一个电话)
CREATE TABLE employee_phones (
emp_id INT,
phone VARCHAR(20),
PRIMARY KEY (emp_id, phone),
FOREIGN KEY (emp_id) REFERENCES employees(emp_id)
);
-- 技能表(一行一个技能)
CREATE TABLE employee_skills (
emp_id INT,
skill VARCHAR(20),
PRIMARY KEY (emp_id, skill),
FOREIGN KEY (emp_id) REFERENCES employees(emp_id)
);
INSERT INTO employees (emp_name) VALUES ('大翔'), ('白歌');
INSERT INTO employee_phones VALUES
(1, '13800138001'),
(1, '13900139001'),
(2, '13900139002');
INSERT INTO employee_skills VALUES
(1, 'Java'),
(1, 'MySQL'),
(1, 'Linux'),
(2, 'Python'),
(2, 'MySQL');
操作后数据状态:
employees:
| emp_id | emp_name |
|---|---|
| 1 | 大翔 |
| 2 | 白歌 |
employee_phones:
| emp_id | phone |
|---|---|
| 1 | 13800138001 |
| 1 | 13900139001 |
| 2 | 13900139002 |
employee_skills:
| emp_id | skill |
|---|---|
| 1 | Java |
| 1 | MySQL |
| 1 | Linux |
| 2 | Python |
| 2 | MySQL |
结果解读:每个单元格只包含一个值。查询"会 MySQL 的员工"变得简单且高效:
SELECT e.emp_name
FROM employees e
JOIN employee_skills s ON e.emp_id = s.emp_id
WHERE s.skill = 'MySQL';
| emp_name |
|---|
| 大翔 |
| 白歌 |
第二范式(2NF):消除部分依赖
第二范式要求:在满足 1NF 的基础上,非主键列必须完全依赖于整个主键,而不是主键的一部分。
2NF 只针对复合主键(多列组成的主键)的表。单主键表自动满足 2NF。
场景三:违反 2NF 的设计
当前数据状态:记录员工参与项目及其绩效的表。
CREATE TABLE project_scores_bad (
emp_id INT,
project_id INT,
emp_name VARCHAR(20), -- 员工姓名
project_name VARCHAR(50), -- 项目名称
score DECIMAL(5,2),
dept VARCHAR(20), -- 员工部门
PRIMARY KEY (emp_id, project_id)
);
INSERT INTO project_scores_bad VALUES
(1, 101, '大翔', '乐途官网重构', 95, '技术部'),
(1, 102, '大翔', '数据中台建设', 88, '技术部'),
(2, 101, '白歌', '乐途官网重构', 92, '产品部');
SELECT * FROM project_scores_bad;
| emp_id | project_id | emp_name | project_name | score | dept |
|---|---|---|---|---|---|
| 1 | 101 | 大翔 | 乐途官网重构 | 95 | 技术部 |
| 1 | 102 | 大翔 | 数据中台建设 | 88 | 技术部 |
| 2 | 101 | 白歌 | 乐途官网重构 | 92 | 产品部 |
问题分析:
- 主键是
(emp_id, project_id),但emp_name和dept只依赖于emp_id(主键的一部分) project_name只依赖于project_id(主键的另一部分)- 数据冗余:大翔的姓名和部门出现了 2 次;"乐途官网重构"出现了 2 次
- 更新异常:大翔调部门后,需要更新多行;漏改一行就会产生数据不一致
- 插入异常:新员工还没分配项目时,无法插入(缺少
project_id,主键不完整) - 删除异常:删除大翔的某个项目记录时,可能连带丢失他的姓名和部门信息
场景四:拆分为 2NF
操作语句:将部分依赖的属性拆分到独立的表中。
-- 员工表
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20),
dept VARCHAR(20)
);
-- 项目表
CREATE TABLE projects (
project_id INT PRIMARY KEY,
project_name VARCHAR(50)
);
-- 员工项目成绩表(只保留完全依赖复合主键的属性)
CREATE TABLE project_scores (
emp_id INT,
project_id INT,
score DECIMAL(5,2),
PRIMARY KEY (emp_id, project_id),
FOREIGN KEY (emp_id) REFERENCES employees(emp_id),
FOREIGN KEY (project_id) REFERENCES projects(project_id)
);
INSERT INTO employees VALUES
(1, '大翔', '技术部'),
(2, '白歌', '产品部');
INSERT INTO projects VALUES
(101, '乐途官网重构'),
(102, '数据中台建设');
INSERT INTO project_scores VALUES
(1, 101, 95),
(1, 102, 88),
(2, 101, 92);
操作后数据状态:
employees:
| emp_id | emp_name | dept |
|---|---|---|
| 1 | 大翔 | 技术部 |
| 2 | 白歌 | 产品部 |
projects:
| project_id | project_name |
|---|---|
| 101 | 乐途官网重构 |
| 102 | 数据中台建设 |
project_scores:
| emp_id | project_id | score |
|---|---|---|
| 1 | 101 | 95 |
| 1 | 102 | 88 |
| 2 | 101 | 92 |
结果解读:
emp_name和dept移入employees表,只存一次project_name移入projects表,只存一次project_scores只保留score——它确实同时依赖emp_id和project_id(同一员工在不同项目分数不同)- 大翔调部门只需改
employees表一行 - 新员工未分配项目也能先录入
employees表
常见误区
| 误区 | 正解 |
|---|---|
| "单主键表不需要考虑 2NF" | 正确。2NF 的问题只出现在复合主键表中,单主键表自动满足 2NF。 |
| "1NF 就是每个字段只存一个值" | 准确说是"每个单元格只存一个值"。同一列在不同行可以存不同值,但一个单元格不能是列表或集合。 |
| "满足范式就一定是好设计" | 不一定。过度范式化会导致表过多、JOIN 复杂。实际项目中常有意反范式化以换取查询性能。 |
面试考点
Q:1NF 的核心要求是什么?
每个属性值都是原子的、不可再分的。常见违反:用逗号分隔存储多个值(如
phones = '13800138001,13900139001')。解决:拆分为独立的关联表。
Q:2NF 解决什么问题?
消除非主键属性对复合主键的部分依赖。例如
(emp_id, project_id)为主键时,emp_name只依赖emp_id,应拆入员工表。
Q:单主键表会有 2NF 问题吗?
不会。2NF 针对复合主键。单主键表中,非主键列要么依赖整个主键,要么不依赖——不存在"部分依赖"。
小结
- 1NF:每个单元格只包含一个原子值,消除多值属性
- 2NF:消除非主键列对复合主键的部分依赖,将只依赖主键一部分的属性拆分到独立表
- 范式化的收益:减少冗余、避免更新/插入/删除异常
- 范式化的代价:表数量增加,查询需要更多 JOIN
清理测试数据:
DROP TABLE IF EXISTS employee_skills;
DROP TABLE IF EXISTS employee_phones;
DROP TABLE IF EXISTS project_scores;
DROP TABLE IF EXISTS projects;
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS employees_bad;
DROP TABLE IF EXISTS project_scores_bad;
下一章引子:1NF 和 2NF 消除了原子性和部分依赖问题,但还有一种更隐蔽的冗余——传递依赖。第三范式和 BC 范式就是为此而生。