数据库三大范式是什么?追问 1:范式设计是为了解决什么问题? 追问 2:范式设计有什么缺点?

可以把数据库范式理解成:整理宿舍的规则

刚搬进宿舍时,衣服、书、零食、充电器全堆在床上,拿东西很方便,但时间一长就会出现三个问题:

  • 同样的东西到处重复;
  • 改一个地方,其他地方忘了改;
  • 扔掉一件东西时,不小心把有用信息也一起扔了。

数据库范式做的事情,就是把这张“万能大表”拆成几张职责明确的小表。

我们用大学生最熟悉的学生选课系统来解释。


一、先看一张设计很差的表

假设学校把所有信息都塞进一张表:

学号 学生姓名 手机号 课程编号 课程名称 教师姓名 教师电话 成绩
1001 张三 1380001 C01 数据库 王老师 1391001 85
1001 张三 1380001 C02 Java 李老师 1391002 90
1002 李四 1380002 C01 数据库 王老师 1391001 78
1003 王五 1380003 C01 数据库 王老师 1391001 92

乍一看挺方便:查学生、课程、老师、成绩,都在一张表里。

但问题已经出现了。

张三的信息重复了两次:

张三、1380001

数据库课程的信息重复了三次:

C01、数据库、王老师、1391001

学生越多、选课越多,重复数据就越严重。

范式就是一步一步拆掉这些不合理的重复。


二、第一范式:每个格子只放一个值

第一范式,简称 1NF

核心要求是:

表中的每一列都应该是不可再拆分的单一值,不能在一个单元格中存放多个同类数据。

一个违反第一范式的例子

假设学生表这样设计:

学号 姓名 手机号 所选课程
1001 张三 1380001 数据库、Java、操作系统
1002 李四 1380002 数据库、计算机网络

问题在于:

所选课程 = 数据库、Java、操作系统

一个格子里放了三个课程。

这会导致查询非常麻烦。

例如,想查询“所有选择数据库课程的学生”,可能只能写模糊匹配:

SELECT *
FROM student
WHERE courses LIKE '%数据库%';

这样做不仅效率低,还可能误匹配。

更麻烦的是,如果课程名称中本身包含逗号,或者使用了不同分隔符,处理起来就更混乱。

符合第一范式的设计

把一名学生的每门课程拆成独立的一行:

学号 姓名 手机号 课程
1001 张三 1380001 数据库
1001 张三 1380001 Java
1001 张三 1380001 操作系统
1002 李四 1380002 数据库
1002 李四 1380002 计算机网络

现在每个格子只有一个值。

查询数据库课程的学生就很简单:

SELECT *
FROM student_course
WHERE course_name = '数据库';

现实生活类比

假设食堂点餐单上有一个“菜品”栏。

不规范的写法是:

宫保鸡丁、米饭、可乐

全部挤在同一个格子里。

规范的写法是每种菜品单独记录:

订单 001:宫保鸡丁
订单 001:米饭
订单 001:可乐

这样才能方便地统计:

  • 宫保鸡丁卖了多少份;
  • 哪些订单买了可乐;
  • 米饭今天的销售额是多少。

第一范式解决什么问题

主要解决的是:

  • 一个字段保存多个值;
  • 数据难以查询;
  • 数据难以排序和统计;
  • 数据格式不统一。

不过,仅仅满足第一范式还远远不够。

因为刚才拆成多行后,张三的姓名和手机号还是重复了很多次。

这就需要第二范式。


三、第二范式:非主键字段必须依赖整个主键

第二范式,简称 2NF

它必须先满足第一范式,然后要求:

表中的非主键字段,必须完全依赖整个主键,不能只依赖联合主键的一部分。

这句话听起来有点绕,我们慢慢拆开。

什么是联合主键

在选课表中,单独使用学号不能唯一确定一条记录。

因为一个学生可以选择多门课程:

1001:数据库
1001:Java

单独使用课程编号也不能唯一确定一条记录。

因为一门课程可以被多个学生选择:

1001:数据库
1002:数据库

所以需要把:

学号 + 课程编号

放在一起,才能唯一确定一条选课记录。

这个组合就是联合主键。

例如:

1001 + C01

代表张三选择了数据库课程。

现在来看这张表:

学号 课程编号 学生姓名 手机号 课程名称 教师姓名 成绩
1001 C01 张三 1380001 数据库 王老师 85
1001 C02 张三 1380001 Java 李老师 90
1002 C01 李四 1380002 数据库 王老师 78

它的联合主键是:

学号 + 课程编号

接下来分析每个字段依赖谁。

学生姓名依赖谁?

只要知道学号,就能知道学生姓名:

学号 1001 → 张三

不需要知道课程编号。

所以:

学生姓名只依赖学号

手机号依赖谁?

也是只依赖学号:

学号 1001 → 1380001

课程名称依赖谁?

只要知道课程编号,就能知道课程名称:

课程编号 C01 → 数据库

不需要知道学号。

教师姓名依赖谁?

在这个简化例子中,假设一门课程由一位老师负责,那么教师姓名只依赖课程编号。

成绩依赖谁?

成绩必须同时知道学生和课程。

张三的数据库成绩是 85,张三的 Java 成绩是 90;李四的数据库成绩是 78。

所以:

学号 + 课程编号 → 成绩

成绩依赖完整的联合主键。

问题在哪里

联合主键是:

学号 + 课程编号

但:

学生姓名、手机号只依赖学号
课程名称、教师姓名只依赖课程编号

它们都只依赖联合主键的一部分。

这叫做:

部分依赖。

违反第二范式。


四、如何改造成第二范式

把不同职责的数据拆开。

学生表

student
学号 学生姓名 手机号
1001 张三 1380001
1002 李四 1380002

主键是学号。

学生姓名和手机号都依赖学号。

课程表

course
课程编号 课程名称 教师姓名
C01 数据库 王老师
C02 Java 李老师

主键是课程编号。

课程名称和教师姓名都依赖课程编号。

选课表

enrollment
学号 课程编号 成绩
1001 C01 85
1001 C02 90
1002 C01 78

联合主键是:

学号 + 课程编号

成绩依赖这两个字段共同组成的主键。

现在每张表都只负责一件事:

  • 学生表负责学生信息;
  • 课程表负责课程信息;
  • 选课表负责学生和课程之间的关系以及成绩。

现实生活类比

想象学校以前使用一本巨大的登记册。

每当张三选择一门课,就要重新抄一遍:

张三、手机号、家庭地址、专业、课程、教师、成绩

张三选十门课,个人信息就抄十遍。

第二范式相当于告诉教务处:

学生的个人档案只保存一份。选课登记表只写学号,不要每次都重新抄学生的全部资料。

就像坐飞机时,航空公司不会在每张行李标签上重新打印你的完整身份证档案,只写一个能关联到你的编号。


五、第三范式:非主键字段之间不能互相决定

第三范式,简称 3NF

它必须先满足第二范式,然后要求:

非主键字段不能依赖另一个非主键字段,也就是不能存在传递依赖。

我们继续看课程表:

课程编号 课程名称 教师编号 教师姓名 教师电话
C01 数据库 T01 王老师 1391001
C02 Java T02 李老师 1391002
C03 操作系统 T01 王老师 1391001

主键是课程编号。

从课程编号可以找到教师编号:

课程编号 → 教师编号

从教师编号又可以找到教师姓名和电话:

教师编号 → 教师姓名、教师电话

所以形成了这样一条依赖链:

课程编号
→ 教师编号
→ 教师姓名、教师电话

课程编号并不是直接决定教师电话,而是通过教师编号间接决定。

这就叫:

传递依赖。

为什么这是问题

王老师同时教授数据库和操作系统。

于是王老师的信息需要重复保存两次:

T01、王老师、1391001

假设王老师换了电话号码。

管理员必须修改两行:

数据库课程中的电话号码
操作系统课程中的电话号码

如果只修改了一行,就出现了:

同一个王老师,有两个电话号码

数据库自己都不知道哪个是真的。


六、如何改造成第三范式

把教师信息单独拆成教师表。

课程表

课程编号 课程名称 教师编号
C01 数据库 T01
C02 Java T02
C03 操作系统 T01

教师表

教师编号 教师姓名 教师电话
T01 王老师 1391001
T02 李老师 1391002

现在:

  • 课程表只保存课程信息;
  • 教师表只保存教师信息;
  • 课程表通过教师编号关联教师表。

王老师换电话时,只需要修改一处:

UPDATE teacher
SET phone = '1399999'
WHERE teacher_id = 'T01';

所有由王老师教授的课程,都能查询到新的号码。

现实生活类比

假设每个班级的课表上都写着老师的姓名、电话号码、家庭地址和办公室号码。

一位老师教五个班,就要在五张课表中重复写五遍。

老师换办公室时,需要修改五张课表。少改一张,就有人跑错办公室。

第三范式的做法是:

课表只写教师编号,老师的详细资料放在教师通讯录里。

这样教师资料只保存一份。


七、把三大范式放在一起理解

可以用三句话记忆:

第一范式

一个格子只放一个值。

解决“一个字段塞了多个数据”的问题。

例如不能写:

课程:数据库、Java、操作系统

应该拆成多条记录。

第二范式

每个非主键字段都要依赖整个主键。

重点处理联合主键中的部分依赖。

例如选课表的主键是:

学号 + 课程编号

成绩依赖两者,但学生姓名只依赖学号,所以学生姓名应该放进学生表。

第三范式

非主键字段不能依赖另一个非主键字段。

例如:

课程编号 → 教师编号 → 教师电话

教师电话应该放进教师表,而不是放在课程表里。


八、范式设计到底是为了解决什么问题

范式设计最重要的目标,是:

减少数据冗余,避免数据不一致,并解决数据增删改时出现的异常。

这些异常通常分成三类。

1. 更新异常

原表中,王老师的信息重复出现很多次。

课程 教师 电话
数据库 王老师 1391001
操作系统 王老师 1391001
数据结构 王老师 1391001

王老师换电话时,需要修改三行。

如果漏改了一行:

课程 教师 电话
数据库 王老师 1399999
操作系统 王老师 1399999
数据结构 王老师 1391001

同一位老师出现两个电话。

这就是更新异常。

规范化以后,教师信息只保存一份,修改一次即可。


2. 插入异常

假设学校新招聘了赵老师,但他暂时还没有安排课程。

如果教师信息只能存放在课程表中:

课程编号 课程名称 教师姓名 教师电话

那么没有课程编号,就无法录入赵老师。

这很奇怪:

明明学校已经有这位老师,却因为他还没有教课,所以数据库不允许保存他。

这就是插入异常。

拆出教师表以后,即使赵老师没有课程,也可以先录入:

教师编号 教师姓名 教师电话
T03 赵老师 1391003

3. 删除异常

假设李老师目前只教授 Java。

课程表中只有这一条记录:

课程编号 课程名称 教师姓名 教师电话
C02 Java 李老师 1391002

现在学校取消 Java 课程。

删除这条课程记录时,李老师的信息也跟着全部消失了。

但现实中:

课程取消了,不代表老师离职了。

这就是删除异常。

拆分教师表以后,删除课程只会删除课程信息,不会删除教师档案。


九、范式设计的缺点是什么

范式不是越高越好。

它解决了重复和一致性问题,但代价是:

表变多了,查询变复杂了,关联成本也提高了。

缺点一:查询需要更多 JOIN

没有拆表时,查询学生成绩可能很简单:

SELECT *
FROM student_course_info;

规范化后,需要连接多个表:

SELECT
  s.student_name,
  c.course_name,
  t.teacher_name,
  e.score
FROM enrollment e
JOIN student s
  ON e.student_id = s.student_id
JOIN course c
  ON e.course_id = c.course_id
JOIN teacher t
  ON c.teacher_id = t.teacher_id;

这条 SQL 同时连接了:

  • 选课表;
  • 学生表;
  • 课程表;
  • 教师表。

对于初学者来说,理解和维护难度会提高。


缺点二:复杂查询可能变慢

每次查询都要进行多表关联。

如果表中有几千万条数据,而索引设计又不好,多个 JOIN 可能消耗较多资源。

例如电商首页要展示:

  • 商品名称;
  • 品牌名称;
  • 店铺名称;
  • 当前价格;
  • 销量;
  • 商品分类;
  • 活动信息。

如果所有信息都严格拆成很多张表,每次打开页面都进行十几张表的关联,性能可能不理想。

所以实际项目中,可能会适当保存一些重复字段。

例如订单明细中,通常会保存购买当时的商品名称和价格:

订单号 商品编号 商品名称快照 成交价格
O001 P001 黑色双肩包 199

即使商品表中的商品后来改名或者涨价,历史订单仍然应该显示购买当时的信息。

这种冗余不是设计失误,而是有业务目的的反范式设计。


缺点三:表太多,开发和维护更复杂

假设把每个概念都拆得特别细:

学生表
学生姓名表
学生电话表
电话号码类型表
学生地址表
省份表
城市表
区县表
街道表

只是查询一名学生的信息,可能就要关联七八张表。

理论上很规范,实际开发人员看了可能会头疼。

这叫:

过度规范化。

范式应该帮助业务,而不是让数据库变成迷宫。


缺点四:统计分析不方便

业务系统通常喜欢规范化,因为需要频繁增删改。

但数据报表和数据仓库往往更喜欢宽表。

比如校长想看一张报表:

学生 专业 课程 教师 成绩 学期

如果数据分散在很多表中,每次统计都需要复杂关联。

因此数据仓库经常会使用星型模型,允许一定的数据冗余,从而提高统计查询效率。


十、现实项目中应该怎么选择

可以把数据库设计分成两个目标。

业务系统:更重视一致性

例如:

  • 银行账户;
  • 用户信息;
  • 商品库存;
  • 教务系统;
  • 医院挂号系统。

这些系统经常修改数据,非常害怕出现“不一致”。

通常会尽量遵循第三范式。

查询和报表系统:更重视读取速度

例如:

  • 销售大屏;
  • 经营分析报表;
  • 推荐系统特征表;
  • 数据仓库宽表。

这些系统主要用于查询,更新相对较少。

可以适当反范式化,用一定的数据冗余换取更快的查询速度。

所以真实工程中不是简单地说:

范式越高越好

而是:

在数据一致性、开发复杂度和查询性能之间做平衡。


十一、面试回答版本

面试时可以这样回答:

第一范式要求字段具有原子性,也就是一个字段只保存一个值,不能保存多个同类数据。
第二范式在第一范式的基础上,要求非主键字段完全依赖整个主键,不能只依赖联合主键的一部分,主要用于消除部分依赖。
第三范式在第二范式的基础上,要求非主键字段不能依赖其他非主键字段,主要用于消除传递依赖。

范式设计的目的,是减少数据冗余,避免数据不一致,并解决更新异常、插入异常和删除异常。

它的缺点是会把数据拆分到更多表中,使 SQL 中的 JOIN 增多,查询和开发复杂度提高,有时也会影响查询性能。因此实际项目中通常以第三范式为基础,再根据性能和业务需求适当进行反范式设计。

最后记住这个顺口溜:

一范式:一个格子一个值。
二范式:依赖主键要完整。
三范式:非主键之间别传递。

posted @ 2026-07-27 16:06  余风0903  阅读(2)  评论(0)    收藏  举报