第三范式与 BC 范式
导学
满足了第一范式和第二范式,表结构已经比较规范。但如果非主键列之间存在"传递依赖"——A 决定 B,B 又决定 C——那么 C 实际上间接依赖于主键,这会导致冗余和更新异常。第三范式和 BC 范式就是用来消除这种传递依赖的。
第三范式(3NF):消除传递依赖
第三范式要求:在满足 2NF 的基础上,非主键列之间不能存在传递依赖——每个非主键列必须直接依赖于主键,不能通过其他非主键列"间接"依赖。
场景一:违反 3NF 的设计
当前数据状态:员工表包含了部门信息。
CREATE TABLE employees_bad (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20),
dept_id INT,
dept_name VARCHAR(20), -- 部门名称
dept_location VARCHAR(20) -- 部门办公地点
);
INSERT INTO employees_bad VALUES
(1, '大翔', 10, '技术部', '3号楼5层'),
(2, '白歌', 10, '技术部', '3号楼5层'),
(3, '孔蓝', 20, '产品部', '2号楼3层');
SELECT * FROM employees_bad;
| emp_id | emp_name | dept_id | dept_name | dept_location |
|---|---|---|---|---|
| 1 | 大翔 | 10 | 技术部 | 3号楼5层 |
| 2 | 白歌 | 10 | 技术部 | 3号楼5层 |
| 3 | 孔蓝 | 20 | 产品部 | 2号楼3层 |
问题分析:
- 主键是
emp_id dept_name和dept_location依赖于dept_id,而dept_id又依赖于emp_id- 所以
dept_name传递依赖于emp_id:emp_id -> dept_id -> dept_name - 数据冗余:技术部的名称和地点出现了 2 次
- 更新异常:技术部搬到 4 号楼,需要改多行;漏改一行就数据不一致
- 插入异常:新部门还没有员工时,无法录入部门信息(没有
emp_id) - 删除异常:删除大翔和白歌后,技术部的信息就彻底消失了
场景二:拆分为 3NF
操作语句:将传递依赖链拆分为独立的表。
-- 部门表(消除传递依赖的中间环节)
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(20),
dept_location VARCHAR(20)
);
-- 员工表(只保留直接依赖主键的属性)
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20),
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
INSERT INTO departments VALUES
(10, '技术部', '3号楼5层'),
(20, '产品部', '2号楼3层');
INSERT INTO employees VALUES
(1, '大翔', 10),
(2, '白歌', 10),
(3, '孔蓝', 20);
操作后数据状态:
departments:
| dept_id | dept_name | dept_location |
|---|---|---|
| 10 | 技术部 | 3号楼5层 |
| 20 | 产品部 | 2号楼3层 |
employees:
| emp_id | emp_name | dept_id |
|---|---|---|
| 1 | 大翔 | 10 |
| 2 | 白歌 | 10 |
| 3 | 孔蓝 | 20 |
结果解读:
dept_name和dept_location移入departments表,每个部门只存一次- 技术部搬家只需改
departments表一行 - 可以先创建部门再招聘员工,也可以保留空部门
- 删除所有技术部员工后,部门信息仍在
departments表中
查询员工及其部门信息:
SELECT e.emp_name, d.dept_name, d.dept_location
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;
| emp_name | dept_name | dept_location |
|---|---|---|
| 大翔 | 技术部 | 3号楼5层 |
| 白歌 | 技术部 | 3号楼5层 |
| 孔蓝 | 产品部 | 2号楼3层 |
BC 范式(BCNF):消除主键内的决定关系
BC 范式要求:在满足 3NF 的基础上,每一个决定因素(determinant)都必须是候选键。
BCNF 比 3NF 更严格。3NF 允许主键外的决定因素存在(只要被决定的属性是主键的一部分或传递依赖),而 BCNF 不允许任何非候选键的决定因素。
场景三:违反 BCNF 的设计
当前数据状态:记录员工及其直属上级的表。假设每个员工只有一个直属上级,每个上级只负责一个部门。
CREATE TABLE emp_manager_bad (
emp_id INT,
emp_name VARCHAR(20),
manager_id INT,
manager_name VARCHAR(20),
dept VARCHAR(20),
PRIMARY KEY (emp_id)
);
INSERT INTO emp_manager_bad VALUES
(1, '大翔', 100, '王经理', '技术部'),
(2, '白歌', 100, '王经理', '技术部'),
(3, '孔蓝', 200, '李经理', '产品部');
SELECT * FROM emp_manager_bad;
| emp_id | emp_name | manager_id | manager_name | dept |
|---|---|---|---|---|
| 1 | 大翔 | 100 | 王经理 | 技术部 |
| 2 | 白歌 | 100 | 王经理 | 技术部 |
| 3 | 孔蓝 | 200 | 李经理 | 产品部 |
问题分析:
- 候选键是
emp_id - 函数依赖关系:
emp_id -> emp_name, manager_id, manager_name, dept(主键决定一切)manager_id -> manager_name, dept(上级 ID 决定上级姓名和部门)
manager_id不是候选键,但它决定了manager_name和dept- 这满足 3NF(
manager_name和dept不是主键的一部分,且不是传递依赖——它们直接依赖于manager_id),但不满足 BCNF - 冗余:王经理的姓名和部门出现了 2 次
- 更新异常:王经理调部门后需改多行
场景四:拆分为 BCNF
操作语句:将非候选键的决定因素拆分为独立表。
-- 上级/经理表
CREATE TABLE managers (
manager_id INT PRIMARY KEY,
manager_name VARCHAR(20),
dept VARCHAR(20)
);
-- 员工表(只保留 emp_id 直接决定的属性)
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20),
manager_id INT,
FOREIGN KEY (manager_id) REFERENCES managers(manager_id)
);
INSERT INTO managers VALUES
(100, '王经理', '技术部'),
(200, '李经理', '产品部');
INSERT INTO employees VALUES
(1, '大翔', 100),
(2, '白歌', 100),
(3, '孔蓝', 200);
操作后数据状态:
managers:
| manager_id | manager_name | dept |
|---|---|---|
| 100 | 王经理 | 技术部 |
| 200 | 李经理 | 产品部 |
employees:
| emp_id | emp_name | manager_id |
|---|---|---|
| 1 | 大翔 | 100 |
| 2 | 白歌 | 100 |
| 3 | 孔蓝 | 200 |
结果解读:
manager_id -> manager_name, dept的决定关系被封装在managers表中manager_id在managers表中是主键(候选键),满足 BCNF- 王经理调部门只需改
managers表一行 - 可以先录入经理信息,再分配下属员工
3NF vs BCNF 的区别
| 特性 | 3NF | BCNF |
|---|---|---|
| 核心要求 | 消除传递依赖 | 消除所有非候选键的决定因素 |
| 严格程度 | 较宽松 | 更严格 |
| 实际差异 | 大多数满足 3NF 的表也满足 BCNF | 少数 3NF 表不满足 BCNF(如场景三) |
| 设计目标 | 消除冗余和异常 | 彻底消除决定因素层面的冗余 |
实际工作中,3NF 通常已经足够。BCNF 是理论上的更优目标,但在某些场景下(如为了保留函数依赖而接受少量冗余),3NF 比 BCNF 更实用。
常见误区
| 误区 | 正解 |
|---|---|
| "3NF 和 BCNF 是一回事" | 不是。BCNF 更严格,要求所有决定因素都是候选键。少数表满足 3NF 但不满足 BCNF。 |
| "满足 BCNF 就一定满足 3NF" | 是的。BCNF 是 3NF 的加强版,满足 BCNF 自动满足 3NF。 |
| "所有表都必须做到 BCNF" | 不必。3NF 在工程实践中通常已足够,过度拆分会增加 JOIN 复杂度。 |
面试考点
Q:3NF 的核心要求是什么?
消除传递依赖。非主键列必须直接依赖于主键,不能通过其他非主键列间接依赖。例如
emp_id -> dept_id -> dept_name中,dept_name应拆入部门表。
Q:3NF 和 BCNF 的区别?
3NF 允许非主键属性之间存在决定关系(只要被决定的不是主键的一部分);BCNF 要求所有决定因素都必须是候选键。BCNF 更严格,但工程上 3NF 通常够用。
Q:一个表可能满足 3NF 但不满足 BCNF 吗?
可能。典型场景:表有多个候选键,且存在非候选键的决定因素。例如
emp_manager表中manager_id -> dept,manager_id不是候选键。
小结
- 3NF:消除传递依赖,非主键列必须直接依赖主键
- BCNF:消除所有非候选键的决定因素,比 3NF 更严格
- 工程实践中 3NF 通常足够,过度范式化会增加 JOIN 开销
- 范式是设计指南而非绝对法则,实际项目中常有意反范式化
清理测试数据:
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
DROP TABLE IF EXISTS managers;
DROP TABLE IF EXISTS employees_bad;
DROP TABLE IF EXISTS emp_manager_bad;
下一章引子:范式化消除了冗余,但查询时不得不写大量 JOIN。在某些读多写少的场景下,有意保留一些冗余——这就是反范式设计。