本文介绍一下关系型数据库的三大范式。定义网上已经讲得很多了,这里主要用一张学生选课表,看不满足范式时会出现什么问题,以及怎么拆表。
范式
范式(Normal Form)是减少数据冗余、避免数据异常的一组设计规则。数据异常一般指下面三种情况:
- 插入异常:想插入一条数据,却因为缺少其他信息而插不进去。
- 更新异常:同一份信息存了很多份,改的时候漏改一处,数据就不一致了。
- 删除异常:删掉一条记录,把本不想删的信息也一起删掉了。
三大范式是逐层递进的。满足 1NF 之后才能谈 2NF,满足 2NF 之后才能谈 3NF。
一个例子
假设要做学生选课系统,把所有信息都放在一张表里:
| 学号 | 姓名 | 系别 | 系主任 | 课程 | 成绩 |
|---|---|---|---|---|---|
| 1001 | 张三 | 计算机 | 王教授 | 数据库,操作系统 | 90, 85 |
| 1002 | 李四 | 计算机 | 王教授 | 数据库 | 88 |
| 1003 | 王五 | 数学 | 陈教授 | 高等代数 | 92 |
这张表能存数据,但问题不少,下面按三大范式逐步改。
第一范式(1NF):字段不可再分
定义:表中每一列都是原子的,不可再分。
上面这张表的「课程」和「成绩」列,张三那一行写成了 数据库,操作系统,成绩也是 90, 85,一个单元格里放了多个值,不满足 1NF。
这样存的话,用 SQL 不好查「张三数据库考了多少分」,也不方便对单科成绩排序、求平均。
改法:把多值拆开,一门课的成绩占一行。
| 学号 | 姓名 | 系别 | 系主任 | 课程 | 成绩 |
|---|---|---|---|---|---|
| 1001 | 张三 | 计算机 | 王教授 | 数据库 | 90 |
| 1001 | 张三 | 计算机 | 王教授 | 操作系统 | 85 |
| 1002 | 李四 | 计算机 | 王教授 | 数据库 | 88 |
| 1003 | 王五 | 数学 | 陈教授 | 高等代数 | 92 |
每个单元格都是单一值,就满足 1NF 了。这张表的主键是(学号,课程):只有学号定不了一行(张三有两行),只有课程也定不了一行(数据库有两行),两个一起才能确定一条选课记录。
第二范式(2NF):消除对主键的部分依赖
定义:在满足 1NF 的前提下,非主键字段必须完全依赖于整个主键,不能只依赖主键的一部分。
2NF 只在联合主键时才有意义。这里主键是(学号,课程),非主键字段的依赖关系如下:
- 姓名、系别、系主任:知道学号就能确定,和课程无关,只依赖主键的一部分(学号),属于部分依赖,不满足 2NF。
- 成绩:要同时知道学号和课程才能确定,是完全依赖。
部分依赖会带来前面说的异常。张三选了两门课,「姓名、系别、系主任」就会存两遍,改一处漏一处就是更新异常。新生还没选课,想登记学生信息却插不进去,因为没有课程就凑不齐主键,这是插入异常。
改法:拆表,把只依赖学号的字段单独放一张表。
学生表(主键:学号)
| 学号 | 姓名 | 系别 | 系主任 |
|---|---|---|---|
| 1001 | 张三 | 计算机 | 王教授 |
| 1002 | 李四 | 计算机 | 王教授 |
| 1003 | 王五 | 数学 | 陈教授 |
选课表(主键:学号 + 课程)
| 学号 | 课程 | 成绩 |
|---|---|---|
| 1001 | 数据库 | 90 |
| 1001 | 操作系统 | 85 |
| 1002 | 数据库 | 88 |
| 1003 | 高等代数 | 92 |
选课表里的成绩依赖整个主键,学生表里的字段依赖学号,两张表都满足 2NF。
第三范式(3NF):消除传递依赖
定义:在满足 2NF 的前提下,非主键字段之间不能有传递依赖,也就是非主键字段不能依赖于另一个非主键字段。
再看学生表,主键是学号:
- 学号 → 系别
- 系别 → 系主任
连起来就是 学号 → 系别 → 系主任。系主任不是直接依赖学号,而是通过系别间接依赖,这就是传递依赖。
计算机系如果有 100 个学生,「王教授」就要存 100 遍。系主任换人时要改 100 行,漏一行数据就不一致(更新异常)。某个系暂时没有学生,这个系和系主任的信息也没地方放(插入异常)。
改法:再拆一张系表,把「系 → 系主任」单独放。
学生表(主键:学号)
| 学号 | 姓名 | 系别 |
|---|---|---|
| 1001 | 张三 | 计算机 |
| 1002 | 李四 | 计算机 |
| 1003 | 王五 | 数学 |
系表(主键:系别)
| 系别 | 系主任 |
|---|---|
| 计算机 | 王教授 |
| 数学 | 陈教授 |
系主任只存一份,换人只改一行,没有学生的系也可以单独维护。三张表都满足 3NF。
小结
- 1NF:每个字段不可再分。
- 2NF:消除非主键字段对主键的部分依赖。联合主键时,字段不能只依赖主键的一部分。
- 3NF:消除非主键字段之间的传递依赖。字段不能通过其他非主键字段间接依赖主键。
也可以记:「1NF 要原子,2NF 消部分,3NF 消传递。」
范式不是越高越好
范式化的目的是消除冗余,表会拆得比较碎,查询时 JOIN 会变多,性能可能下降。
实际开发里有时会做反范式(Denormalization):为了查询速度,故意留一些冗余字段。比如订单表里直接存商品名称,查订单时就不用再 JOIN 商品表。
所以设计表时先按范式拆清楚,需要性能时再有选择地加回冗余。