数据库规范化可以用一句话概括:“同一个事实只存一处。” 而系统地做到这一点的方法,就是 1NF → 2NF → 3NF。
它为什么重要?——因为如果同一条信息分散在多行里,只改了一处、漏改了其他地方,数据立刻就会自相矛盾。这种情况叫做更新异常。下面你可以亲自体验一下。
如果 Prof.A 的办公室变了,在非规范化表里,你必须手动把 2 行都改掉。只要漏掉一行,就会出现“Prof.A 既在 Room 301,又在 Room 502”这样的矛盾。而在 3NF 表里,只改 1 行就结束了。
这其实就是规范化的全部核心——剩下的内容,不过是在判断“哪些列应该放在哪里,才能保证只存一处”的规则。
函数依赖——规范化的判断标准
规范化要看的,是“这一列依赖于谁”。这就叫函数依赖。
比如 student_id → student_name 的意思是:“只要知道学生 ID,姓名就能确定。”
看下面图里的箭头颜色:
- 绿色 = 依赖于整个键(正常)
- 黄色 = 只依赖于键的一部分(部分依赖 → 违反 2NF)
- 红色虚线 = 通过非键列间接依赖(传递依赖 → 违反 3NF)
去掉黄色和红色箭头 = 拆表 = 规范化。
一次看懂 1NF → 2NF → 3NF
下面可以自己点一点,看看表是如何一步步拆开的。红色单元格表示“这一列不应该放在这里”。
1NF:一个单元格里只能有一个值
如果一个单元格里放了 MATH101, CS102 这种多个值,不仅难查,也没法 JOIN。
规则:所有单元格都必须是原子的(只有一个值)。
像 phone1, phone2, phone3 这样的列,本质上也是同样的问题——只是把值藏进了列名里而已。
2NF:消除部分依赖
在复合键 (student_id, course_id) 的表中,student_name 只依赖于 student_id。它不是依赖整个键,而只是依赖其中一部分——这就是部分函数依赖。
规则:所有非键列都必须依赖于整个键。
如果存在部分依赖,就把对应的列拆出去,放到单独的表里。
如果键只有一列呢?那就不可能出现部分依赖,所以只要满足 1NF,就自动满足 2NF。
3NF:消除传递依赖
在 Courses 表中,instructor_office 并不直接依赖 course_id(键)。它是通过 course_id → instructor_id → instructor_office 这条路径确定的——也就是说,非键列依赖于另一个非键列。这就是传递依赖。
规则:非键列必须只通过键来依赖,不能经过其他非键列。
解决方法:把 instructor_office 拆到 Instructors 表中。
别死记,记住一个问题就够了
“所有非键属性都必须依赖于键、依赖于整个键、并且只依赖于键。”
这句话就是全部:
- 依赖于键 = 1NF(事实必须能被键识别)
- 依赖于整个键 = 2NF(依赖的是整个键,而不是键的一部分)
- 只依赖于键 = 3NF(直接依赖键,而不是经过非键列)
考试里遇到规范化题时,只要问这一句:“这一列到底依赖于谁?是整个键、键的一部分,还是某个非键列?”
常见误区
“做到 3NF 就结束了” —— 3NF 是一个很好的起点,但不是终点。有些场景还需要更严格的 BCNF;反过来,也有些场景会为了性能而故意做反规范化。
“表拆得越多越好” —— 规范化的目的不是增加表的数量,而是让“每张表只负责一种事实”。如果没有依赖关系上的理由却硬拆,只会让 JOIN 更复杂。
“NoSQL 就不需要规范化” —— 规范化是关系型 DB 里的术语,但“如何保持重复数据的一致性”这个问题,在任何 DB 里都存在。即使要做反规范化,也应该清楚自己放弃了什么。
自己检查一下
挑你项目里的一张表,按下面做:
- 先写出它的键是什么。
- 检查每个非键列,是依赖整个键,还是只依赖键的一部分。→ 如果是后者,就违反 2NF。
- 检查有没有非键列依赖于其他非键列。→ 如果有,就违反 3NF。
只靠这 3 步,你就能发现大多数规范化问题。