一个设计混乱的数据库就像一个杂乱的房间,用起来非常不方便:东西到处乱放(数据冗余),找件东西要翻遍所有角落(查询困难),扔掉一把旧椅子时,可能会把搭在上面的唯一一件外套也一起扔了(删除异常)。
数据库范式(Normalization) 就是一套“房间整理法则”,指导我们如何合理地组织数据表,以消除冗余、避免上述问题。2NF和3NF是其中最核心、最重要的两条法则。
初始状态:乱糟糟的第一范式 (1NF)
假设我们要设计一个系统来记录学生的选课成绩。最直接的想法可能是把所有信息都塞进一张大表里。这张表符合第一范式(1NF),因为每个单元格都只有一个值,并且没有完全重复的行。但你会发现,它问题重重。
选课成绩表 (符合1NF)
学号 | 课程号 | 学生姓名 | 课程名称 | 授课老师 | 成绩 |
---|---|---|---|---|---|
101 | CS101 | 张三 | 计算机导论 | 王老师 | 92 |
101 | MTH202 | 张三 | 高等数学 | 李老师 | 85 |
102 | CS101 | 李四 | 计算机导论 | 王老师 | 88 |
103 | PHY301 | 王五 | 量子力学 | 赵老师 | 95 |
我们先来分析这张表的“身份”信息:
- 主码 (Primary Key): 要唯一确定一行数据,需要同时知道“哪个学生”和“哪门课”。因为一个学生可以选多门课,一门课也可以被多个学生选。所以,主码是
(学号, 课程号)
这样一个组合。 - 主属性:
学号
,课程号
。 - 非主属性:
学生姓名
,课程名称
,授课老师
,成绩
。
这张表虽然能用,但有严重的“副作用”(我们称之为“异常”):
- 更新异常 (Update Anomaly): 如果学生“张三”改名叫“张大三”,你必须修改所有他出现的行。一旦漏掉一行,数据就不一致了。
- 插入异常 (Insertion Anomaly): 你无法添加一个新生(比如学号104的“赵六”),除非他至少选了一门课。因为主码
(学号, 课程号)
要求课程号
不能为空。 - 删除异常 (Deletion Anomaly): 如果“王五”退掉了他唯一选的课(量子力学),那么他所在的整行数据都会被删除。我们不仅丢失了他的选课记录,连“王五”这个学生本身的信息也从数据库里消失了!
如何解决这些问题?答案就是遵循范式法则进行整理。
第一步整理:达到第二范式 (2NF)
2NF的法则
一个表在符合1NF的基础上,如果所有非主属性都“完全函数依赖”于主码,那么它就符合2NF。
- “完全函数依赖”:听起来很学术,其实很简单。意思就是一个非主属性的值,必须由整个主码来唯一确定,而不是由主码的一部分就能确定。
- 通俗版法则:任何一个非主属性,都不能只依赖于组合主码的一部分。(这条规则只在主码是组合码时才有意义,就像我们这个例子)。
运用2NF法则整理
我们的主码是 (学号, 课程号)
。我们来检查一下每个非主属性:
成绩
: 由什么决定?必须同时知道学号
和课程号
才能确定一个唯一的成绩。所以,成绩
依赖于整个主码。这很好,符合2NF。学生姓名
: 由什么决定?只需要学号
就能确定。学生的姓名和他选了什么课没关系。所以,学生姓名
只依赖于主码的一部分 (学号
)。这很糟糕,违反了2NF。课程名称
和授课老师
: 由什么决定?只需要课程号
就能确定。这和哪个学生来选课也没关系。所以,这两个属性也只依赖于主码的一部分 (课程号
)。这也很糟糕,违反了2NF。
解决方法:拆分!
解决办法就是“分家”。把那些只依赖于部分主码的信息,拆分出去成立自己的独立小家庭(新表)。
- 只依赖于
学号
的信息,放到新的学生表
里。 - 只依赖于
课程号
的信息,放到新的课程表
里。 - 依赖于
(学号, 课程号)
完整组合的信息,留在瘦身后的选课表
里。
学生表 (Students)
学号 (主码) | 学生姓名 |
---|---|
101 | 张三 |
102 | 李四 |
103 | 王五 |
课程表 (Courses)
课程号 (主码) | 课程名称 | 授课老师 |
---|---|---|
CS101 | 计算机导论 | 王老师 |
MTH202 | 高等数学 | 李老师 |
PHY301 | 量子力学 | 赵老师 |
选课表 (Enrollment)
学号 (外码) | 课程号 (外码) | 成绩 |
---|---|---|
101 | CS101 | 92 |
101 | MTH202 | 85 |
102 | CS101 | 88 |
103 | PHY301 | 95 |
(这张表的主码依然是 (学号, 课程号)) |
现在,我们所有的表都符合2NF了,之前提到的那些异常问题也随之解决!张三改名,只需改 学生表
一处。新生赵六可以直接添加到 学生表
,不用非得选课。
第二步整理:达到第三范式 (3NF)
我们离完美还差一步。仔细看刚刚创建的 课程表
,它里面还隐藏着一个问题。
3NF的法则
一个表在符合2NF的基础上,如果不存在“传递函数依赖”,那么它就符合3NF。
- “传递函数依赖”:指的是一个非主属性,不直接依赖于主码,而是通过另一个非主属性间接地依赖于主码。形成了一个依赖链条:
主码 -> 非主属性A -> 非主属性B
。 - 通俗版法则:任何一个非主属性,都不能依赖于其他非主属性。
运用3NF法则整理
为了让问题更明显,我们在 课程表
里加一列“老师所在院系”。
课程表 (符合2NF,但不符合3NF)
课程号 (主码) | 课程名称 | 授课老师 | 老师所在院系 |
---|---|---|---|
CS101 | 计算机导论 | 王老师 | 计算机系 |
MTH202 | 高等数学 | 李老师 | 数学系 |
CS205 | 数据结构 | 王老师 | 计算机系 |
我们来检查主码 课程号
和非主属性之间的依赖关系:
课程名称
直接依赖于课程号
。很好。授课老师
直接依赖于课程号
(为简化,假设一门课只有一个老师)。很好。老师所在院系
: 老师的院系是由课程号
决定的吗?不是! 它是由授课老师决定的。这样就形成了一个依赖链条:
课程号
->授课老师
->老师所在院系
这就是传递依赖,它违反了3NF。
这又会引发老问题!如果“王老师”从“计算机系”调到了“人工智能系”,你就必须修改他教的每一门课的记录,非常麻烦且容易出错。
解决方法:再次拆分!
我们把这个传递依赖链条也拆开,把老师的信息独立出去。
老师表 (Instructors)
授课老师 (主码) | 老师所在院系 |
---|---|
王老师 | 计算机系 |
李老师 | 数学系 |
赵老师 | 物理系 |
新的课程表 (Courses)
课程号 (主码) | 课程名称 | 授课老师 (外码) |
---|---|---|
CS101 | 计算机导论 | 王老师 |
MTH202 | 高等数学 | 李老师 |
CS205 | 数据结构 | 王老师 |
PHY301 | 量子力学 | 赵老师 |
现在,如果王老师调动院系,我们只需在 老师表
中修改唯一的一条记录。至此,我们所有的表都达到了3NF,房间整理完毕!
最终总结
- 1NF:入场券。 确保数据是“原子的”(每个单元格一个值)。
- 2NF:消除部分依赖。 确保所有非主属性都依赖于整个组合主码,而不是它的一部分。(解决方法:把依赖于部分的属性拆出去)。
- 3NF:消除传递依赖。 确保所有非主属性都只依赖于主码,而不是依赖于其他非主属性。(解决方法:把间接依赖的属性拆出去)。
通过遵循这些法则,我们把一张庞大、混乱的表,拆分成了四个干净、清晰、健壮的小表:学生表
、老师表
、课程表
和 选课表
。这就是关系数据库设计的精髓所在。