目录
- 1. 为什么还要谈范式
- 2. 必备基础:函数依赖与键
- 3. 第一范式(1NF):原子性
- 4. 第二范式(2NF):消除部分函数依赖
- 5. 第三范式(3NF):消除传递函数依赖
- 6. BC 范式(BCNF):比 3NF 更严格的一步
- 7. 第四范式(4NF):直面多值依赖
- 8. 工程取舍:范式不是教条
- 8.1 规范化不是免费的
- 8.2 常见的落地姿态
- 8.3 对 1NF 的重新审视
- 9. 落地建议:一个可操作的判断路径
- 10. 总结
1. 为什么还要谈范式
范式(Normal Form)几乎是每个后端开发者在入门时都会接触到、却很少真正吃透的话题。面试时背得出「3NF 就是消除传递依赖」,一到真实建表,却常常在「拆三张表还是合成一张宽表」之间反复摇摆。
范式理论的价值不在炫技,而在于它用一套清晰的规则回答了三个非常实际的问题:
- 一张表到底该放哪些字段?
- 数据冗余会导致什么样的异常?
- 拆表之后,查询代价要付出多少?
本文从 1NF 讲到 4NF,先给出准确的定义,再讨论工程落地时哪些规则应当严守、哪些可以妥协。
2. 必备基础:函数依赖与键
谈范式之前,必须先把两个概念说清楚,否则后面的判断都无从谈起。
函数依赖(Functional Dependency, FD):如果属性集合 X 的值能够唯一确定属性集合 Y 的值,就称 Y 函数依赖于 X,记作 X → Y。
举例:学号能唯一确定学生姓名,即 学号 → 姓名。
候选键(Candidate Key):能唯一标识一行记录的最小属性集合。所谓「最小」,是指去掉其中任意一个属性后,就无法再唯一标识一行。候选键可能不止一个,设计时通常选一个作为主键(Primary Key)。
主属性 / 非主属性:出现在任一候选键中的属性叫主属性;其余叫非主属性。
有了这两个概念,1NF 到 4NF 的递进关系就可以用一句话概括:每一级范式都在上一级的基础上,消除一类额外的「异常依赖」。
3. 第一范式(1NF):原子性
定义:关系的每个属性都是不可再分的原子值。通俗地说,每个字段只存一个值,不能存数组、集合或嵌套结构。
以下表违反 1NF:
| 张三 | 138xxxx, zhangsan@xx.com |
「联系方式」里塞了两个值,查询和更新都非常别扭。改成每一列只存一个原子值即可满足 1NF:
| 张三 | 138xxxx | zhangsan@xx.com |
1NF 是现代关系数据库的底线。需要注意的是,随着 PostgreSQL 的数组/JSONB、MySQL 的 JSON 类型流行,「字段是否必须原子」在工程界产生了新的讨论。严格意义上,把 JSON 塞进一个字段仍然是对 1NF 的突破;但在「列内结构对查询基本无感」的场景,这种突破往往利大于弊。这一点放在第 8 节展开。
4. 第二范式(2NF):消除部分函数依赖
定义:在满足 1NF 的前提下,每个非主属性完全依赖于候选键,而不存在对候选键某一部分的依赖。
典型反例是学生选课表:
CREATE TABLE student_course (
stu_id INT,
course_id INT,
stu_name VARCHAR(50), — 只依赖 stu_id
credit INT, — 只依赖 course_id
PRIMARY KEY (stu_id, course_id)
);
候选键为 (stu_id, course_id),但 stu_name 只依赖 stu_id,credit 只依赖 course_id。这就是部分函数依赖,违反了 2NF。
它带来的问题是:一门课程的学分变了,要改很多行;课程还没学生选时,学分信息无处安放。将表拆为三张,就能消除部分依赖:
CREATE TABLE student (
stu_id INT PRIMARY KEY,
stu_name VARCHAR(50)
);
CREATE TABLE course (
course_id INT PRIMARY KEY,
credit INT
);
CREATE TABLE student_course (
stu_id INT,
course_id INT,
PRIMARY KEY (stu_id, course_id)
);
一句话记忆:主键是复合主键时,检查非主属性是否只依赖复合主键的「一部分」。
5. 第三范式(3NF):消除传递函数依赖
定义:在满足 2NF 的前提下,非主属性之间不存在函数依赖。换句话说,非主属性不能通过另一个非主属性间接依赖候选键。
反例:
CREATE TABLE student (
stu_id INT PRIMARY KEY,
dept_name VARCHAR(50),
dept_head VARCHAR(50) — 依赖 dept_name,而非直接依赖 stu_id
);
这里有 stu_id → dept_name,同时 dept_name → dept_head,于是 stu_id → dept_head 是一条传递依赖。违反 3NF 的后果是:系主任换人时,要更新这个系所有学生的记录;系里暂时没学生时,系与系主任的映射也保存不了。
消除传递依赖的方法是拆出部门表:
CREATE TABLE dept (
dept_name VARCHAR(50) PRIMARY KEY,
dept_head VARCHAR(50)
);
CREATE TABLE student (
stu_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);
3NF 是工程上最常见的「及格线」。绝大多数 OLTP 业务表,把 3NF 做到位,冗余和异常就基本可控了。
6. BC 范式(BCNF):比 3NF 更严格的一步
BCNF(Boyce-Codd Normal Form)常被简单说成「加强版 3NF」。准确的区别在于:3NF 只约束非主属性,而 BCNF 连主属性也要约束。
定义:对于关系中任意非平凡函数依赖 X → Y,X 都必须是超键(即 X 能唯一确定一行)。
注意:3NF 允许一种例外——当 Y 是主属性时,X 可以不是超键。BCNF 不允许任何例外。
经典反例是「学生—教师—课程」表:
CREATE TABLE teaching (
stu_id INT,
teacher VARCHAR(50),
course VARCHAR(50),
PRIMARY KEY (stu_id, teacher)
);
业务约束为:
- 每个教师只教一门课:teacher → course;
- 每名学生每门课只有一位教师:(stu_id, course) → teacher。
此时候选键有两个:(stu_id, teacher) 和 (stu_id, course)。teacher 和 course 都是主属性。函数依赖 teacher → course 中,teacher 不是超键,因此违反 BCNF;但由于 course 是主属性,它并没有违反 3NF。
这就是 3NF 与 BCNF 的真实缝隙:主属性之间也可能存在不该有的依赖。要满足 BCNF,需拆成:
CREATE TABLE teacher_course (
teacher VARCHAR(50) PRIMARY KEY,
course VARCHAR(50)
);
CREATE TABLE stu_teacher (
stu_id INT,
teacher VARCHAR(50),
PRIMARY KEY (stu_id, teacher)
);
实际项目中,能自然落到 BCNF 的表并不多,多数业务表做到 3NF 已经足够,因为「主属性之间的依赖」在真实模型里相对少见。但理解 BCNF,能帮你识别那些「看似合规却总觉得别扭」的表结构。
7. 第四范式(4NF):直面多值依赖
4NF 面对的问题,用函数依赖已经无法描述了。
多值依赖(Multivalued Dependency, MVD):在关系 R(X, Y, Z) 中,若给定 X 的值,Y 的取值集合与 Z 的取值集合相互独立,则称 X →→ Y。例如「员工会说多门语言,也掌握多项技能,语言和技能彼此独立」,就存在 emp_id →→ skill 和 emp_id →→ language。
典型反例:
| 001 | Java | 中文 |
| 001 | Java | 英语 |
| 001 | Python | 中文 |
| 001 | Python | 英语 |
| 002 | SQL | 中文 |
| 002 | SQL | 英语 |
员工 001 会 Java/Python、说中英双语。为了维护「技能与语言相互独立」这一事实,必须用笛卡尔积补全所有组合;少一行,数据就不一致。这类冗余正是多值依赖造成的。
定义:在满足 BCNF 的前提下,对于任意非平凡多值依赖 X →→ Y,X 都必须是超键。
上表的主键是 (emp_id, skill, language),全表都是主属性,因此它满足 BCNF,但 emp_id →→ skill 中的 emp_id 不是超键,违反 4NF。
分解为两张表即可消除:
CREATE TABLE emp_skill (
emp_id INT,
skill VARCHAR(50),
PRIMARY KEY (emp_id, skill)
);
CREATE TABLE emp_language (
emp_id INT,
language VARCHAR(50),
PRIMARY KEY (emp_id, language)
);
4NF 处理的是「同一行里塞了多个彼此独立的一对多关系」。这类问题在实践中通常表现为:一张表同时承担了多个互不相干的集合属性。一旦识别出这种独立性,拆开往往比强行合在一起更自然。
8. 工程取舍:范式不是教条
理论讲完,回到那个最现实的问题:实际建表,到底要遵守到什么程度?
8.1 规范化不是免费的
每提升一级范式,通常都意味着拆出更多表。表拆得越细,写入时的冗余和异常越少,但查询时要 JOIN 的表就越多。在数据量大、并发高的场景里,多表 JOIN 的代价可能远高于冗余带来的维护成本。
因此,规范化的收益是有上限的:当冗余可控、业务逻辑不复杂时,一味追求高范式只会得到一套难写、难调、性能平庸的表结构。
8.2 常见的落地姿态
各场景对范式的要求大致如下:
| OLTP 核心业务 | 3NF / BCNF | 写入频繁,最看重一致性与异常控制 |
| 报表 / 分析宽表 | 1NF / 部分反规范化 | 读多写少,用冗余换查询速度 |
| 日志 / 埋点 | 1NF 即可 | 基本无更新,冗余影响小 |
| 缓存层 / 文档存储 | 不严格要求 | 数据以聚合粒度整体读写 |
8.3 对 1NF 的重新审视
JSON 字段是对 1NF 的直接挑战。一个经典场景是「商品扩展属性」:不同品类的商品属性差异巨大,硬拆成 EAV(实体-属性-值)三张表满足范式,但查询和写入都非常痛苦;直接存一个 JSON 字段,反而是性价比最高的方案。
这里的判断标准不是「是否满足 1NF」,而是:这个列内结构是否需要被数据库频繁检索、约束和更新。如果只是整体存取,JSON 完全可用;如果需要按列内字段做过滤、排序或唯一约束,那最好老老实实拆成独立列。
9. 落地建议:一个可操作的判断路径
面对一张新表,建议按下面顺序自检,而不是从 1NF 机械地推到底:
每拆一次表,都问自己一句:这层拆分换来的是一致性提升,还是徒增 JOIN? 如果答案偏向后者,且冗余带来的更新异常在业务上可接受,那么保持现状就是合理的工程决策。
10. 总结
范式理论提供的是一套精确的「表结构诊断语言」:函数依赖、部分依赖、传递依赖、多值依赖,分别对应 2NF、3NF、BCNF、4NF 要消除的异常。
- 1NF 管原子性;
- 2NF 管复合键下的部分依赖;
- 3NF 管非主属性间的传递依赖;
- BCNF 把约束扩展到主属性;
- 4NF 处理多值依赖。
但它们从来不是越高越好的教条。工程上的成熟做法是:以 3NF/BCNF 为 OLTP 的默认基线,针对读多写少的场景做有意识的反规范化,并为每一处冗余建立清晰的维护约定。理解规则,才能在打破规则时知道代价是什么。
网硕互联帮助中心






评论前必须登录!
注册