- 作者:老汪软件技巧
- 发表时间:2024-09-05 00:02
- 浏览量:
前言
凡做事,必有方法。这个方法也可以称为框架,遇事我们将框架套上去,这事儿就成了。DB设计也是如此,业务中不能够随心所欲,走先污染后治理的路子。当我们在做数据表结构设计的时候,需要遵循一定的规范,这些规范就是我们常说的范式,它帮助我们在设计出更为合理且可扩展的表结构。
在数据库设计范式中,众所周知的是三大范式(1NF、2NF、3NF),这也是实际业务中至少需要遵循的标准,除此之外还有BC范式、第四范式4NF、第五范式5NF。
三大范式
三大范式是业务开发中遵循的最低标准,每个范式之间是递进关系,也就是后面一个范式要基于前一个范式成立的基础上才有意义,不能倒反天罡,例如:
把大象放进冰箱需要三步:首先打开门,然后将大象放进冰箱,最后关上门。
每一步是要以前面一步完成作为基础,你不能先放进冰箱然后再打开门,这是没有意义且行不通的。明白这个概念后我们再来通俗理解这几个范式。
第一范式
要求保证数据表中每一列具有原子性,也就是存储数据列是具有不可再分性,什么意思呢?举例说明。
| 员工ID | 员工 | 技能 | 部门 |
|--------|------|------|--------------------|
| 1 | 张三,男,30 | Vue | 技术部 |
| 1 | 张三,男,30 | React | 技术部 |
| 1 | 张三,男,30 | Node | 技术部 |
| 2 | 李四,女,22 | Word | 财务部 |
| 2 | 李四,女,22 | Excel | 财务部 |
| 3 | 王五,男,35 | Docker | 架构部 |
| 3 | 王五,男,35 | K8s | 架构部 |
上面案例中,员工字段列包含了姓名、性别、年龄三种类型的信息,显然不符合第一范式:原子性。为了符合第一个范式,将员工字段拆分三个字段进行储存,应该将表结构改为:
| 员工ID | 姓名 | 性别 | 年龄 | 技能 | 部门 |
|--------|------|------|------|------|--------|
| 1 | 张三 | 男 | 30 | Vue | 技术部 |
| 1 | 张三 | 男 | 30 | React | 技术部 |
| 1 | 张三 | 男 | 30 | Node | 技术部 |
| 2 | 李四 | 女 | 22 | Word | 财务部 |
| 2 | 李四 | 女 | 22 | Excel | 财务部 |
| 3 | 王五 | 男 | 35 | Docker | 架构部 |
| 3 | 王五 | 男 | 35 | K8s | 架构部 |
将一个字段拆分三个字段之后,此时这个员工表就符合了数据库第一范式。
好,先就此打住!如果开发人员因为前期设计缺少考虑或未按照DB范式设计,硬是生成了这种表结构,那会产生什么问题呢?
满足第一范式后接着继续往下看。
第二范式
一句话理解:每一列都与主键列存在依赖关系。
解释:在第一范式基础上,第二范式要求数据表中每一列的数据,都必须依赖于主键。意味着每一列的数据都与主键存在关系,保证了一张表只能存储同一类型的数据,一张表只做一件事情,依旧来看上面案例。
| 员工ID | 姓名 | 性别 | 年龄 | 技能 | 部门 |
|--------|------|------|------|------|--------|
| 1 | 张三 | 男 | 30 | Vue | 技术部 |
| 1 | 张三 | 男 | 30 | React | 技术部 |
| 1 | 张三 | 男 | 30 | Node | 技术部 |
| 2 | 李四 | 女 | 22 | Word | 财务部 |
| 2 | 李四 | 女 | 22 | Excel | 财务部 |
| 3 | 王五 | 男 | 35 | Docker | 架构部 |
| 3 | 王五 | 男 | 35 | K8s | 架构部 |
技能字段与员工信息似乎属于不同属性的数据,上面表的设计显然不能把员工ID作为主键,应该确保一张表中只储存一种类型的数据。应该拆分下面几张表:
员工信息表:
| 员工ID | 姓名 | 性别 | 年龄 | 部门 |
|--------|------|------|------|--------|
| 1 | 张三 | 男 | 30 | 技术部 |
| 2 | 李四 | 女 | 22 | 财务部 |
| 3 | 王五 | 男 | 35 | 架构部 |
技能表:
| 技能ID | 技能 |
|--------|--------|
| 1 | Vue |
| 2 | React |
| 3 | Node |
| 4 | Word |
| 5 | Excel |
| 6 | Docker |
| 7 | K8s |
员工与技能关联表:
| 员工ID | 技能ID |
|--------|--------|
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 4 |
| 2 | 5 |
| 3 | 6 |
| 3 | 7 |
这样一拆分就清晰多了,员工表和技能表分别存储各自信息,通过关联表维护两者关系,满足了每张表只负责做一件事情,表字段也只与当前主键列存在关系。
此时,员工表和技能表都以自增ID作为主键,确保了每条记录的“唯一性”,在查询时将主键作为唯一索引能够加快查询效率。
OK!那如果按照修改前的表结构存储,会有什么问题?
满足了第二范式后,似乎我们的表设计已经完美了。按照目前的业务复杂度的确如此,接下来新增一些字段,来看第三范式解决了什么问题。
第三范式
一句话理解:非主键列之间不能存在依赖关系。
解释:要求数据表中的非主键列的字段,不能与其他非主键列存在依赖关系,说白了内部不能搞小组织。
在上面满足第二范式的员工表中新增一列部门老大字段:
| 员工ID | 姓名 | 性别 | 年龄 | 部门 | 部门老大 |
|--------|------|------|------|--------|----------|
| 1 | 张三 | 男 | 30 | 技术部 | 张总 |
| 2 | 李四 | 女 | 22 | 财务部 | 李总 |
| 3 | 王五 | 男 | 35 | 架构部 | 王总 |
新增字段之后,此时表结构依旧满足第一范式和第二范式,但看部门与部门老大两个非主键字段就产生了依赖关系呀,一个员工归属于谁管,应该是由这个员工在哪个部门决定,有了部门才有部门老大这个岗位,员工只需存储该部门ID即可,因此需要进一步调整表结构。
理清楚后把表结构拆分如下:
员工表:
| 员工ID | 姓名 | 性别 | 年龄 |
|--------|------|------|------|
| 1 | 张三 | 男 | 30 |
| 2 | 李四 | 女 | 22 |
| 3 | 王五 | 男 | 35 |
部门表:
| 部门ID | 部门名称 | 部门老大 |
|--------|----------|----------|
| 1 | 技术部 | 张总 |
| 2 | 财务部 | 李总 |
| 3 | 架构部 | 王总 |
员工部门关联表:
| 员工ID | 部门ID |
|--------|--------|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
非得拆么?如果不拆分表字段,又会有什么问题?
实际业务中删除数据一般采用软删而不是物理删除,符合第三范式情况下更容易进行标记删除。
BC范式
BC范式名为xxx,是对第三范式的补充,为什么需要补充呢?因为第三范式没有覆盖某些场景,比如联合主键的场景。来看另一个例子:
想象一下,有一个学校的成绩记录表,这个表记录了每个学生上不同课程的成绩,以及这些课程是由哪位老师教的。表中每一行都代表一个学生在某个课程上的一个成绩记录。
| 学生ID | 课程ID | 教师ID | 成绩 |
|--------|--------|--------|------|
| 1 | 101 | 201 | 85 |
| 1 | 102 | 202 | 90 |
| 2 | 101 | 201 | 75 |
| 2 | 102 | 202 | 80 |
在这个表中:
再来看看能不能满足BC范式,它的核心思想是:
每个数据项都应该直接依赖于整个主键,而不是主键的一部分。
我们把学生ID + 课程ID作为主键,那么教师ID和成绩应该直接依赖于这个主键。但是,实际上教师ID并不直接依赖于学生ID + 课程ID,而是依赖于课程ID。
为了满足BC范式,我们可以把原来的表拆分成两个表:
课程教师表:
| 课程ID | 教师ID |
|--------|--------|
| 101 | 201 |
| 102 | 202 |
学生成绩表:
| 学生ID | 课程ID | 成绩 |
|--------|--------|------|
| 1 | 101 | 85 |
| 1 | 102 | 90 |
| 2 | 101 | 75 |
| 2 | 102 | 80 |
第四范式
第四范式是为了解决多值依赖问题而提出的,在此之前先了解什么是多值依赖?
多值依赖的问题至少在三个字段以上的表结构中才会出现,可以想象成一对多、多对一关系,一个字段的值需要通过多个字段才能够确定。
来看一个经典的例子:用户、角色、权限问题。
| user_id | user_name | user_sex | role | permission |
|---------|-----------|----------|-------|------------|
| 1 | Alice | 女 | ADMIN | MANAGE |
| 2 | Bob | 男 | USER | VIEW |
| 3 | Charlie | 男 | ADMIN | EDIT |
| 4 | David | 男 | USER | VIEW |
| 5 | Eve | 女 | USER | VIEW |
| 6 | Frank | 男 | ADMIN | MANAGE |
上述表中,先来看是否满足前面的范式:
从严格来说user_id、role、permission作为联合主键,也可以说它们之间存在依赖关系,比如permission字段依赖role字段,但这种划分的界限不是绝对的,具体以解决实际业务问题作为标准。我们这里暂且认为是满足BC范式。
用户permission字段没法只根据role字段来决定,也无法只根据user_id来决定,而是要role与user_id两个字段共同决定,这就是所谓的多值依赖问题。
再来看另外一个例子,下表是存储学生ID、课程ID、讲师名字、成绩相关信息。
| StudentID | CourseID | Professor | Grade |
|-----------|----------|----------|-------|
| 1 | 101 | Smith | A |
| 1 | 102 | Jones | B |
| 2 | 101 | Smith | A |
| 2 | 103 | Brown | B |
以StudentID + CourseID作为联合主键,是满足上面的几大范式的。
但是会有个问题,StudentID + CourseID无法确定是哪个具体的讲师,只能确定成绩字段,而课程ID与讲师存在依赖关系,一个讲师可能教多个课程,形成一对多关系,这就是多值依赖问题。
消除这种问题的解决方案就是通过拆分表结构,比如前面的用户权限问题,拆分为大名鼎鼎的权限五表。
用户表(`Users`)
| user_id | user_name | user_sex |
|---------|-----------|----------|
| 1 | Alice | 女 |
| 2 | Bob | 男 |
| 3 | Charlie | 男 |
| 4 | David | 男 |
| 5 | Eve | 女 |
| 6 | Frank | 男 |
角色表(`Roles`)
| role_id | role_name |
|---------|-----------|
| 1 | ADMIN |
| 2 | USER |
权限表(`Permissions`)
| permission_id | permission_name |
|---------------|-----------------|
| 1 | MANAGE |
| 2 | VIEW |
| 3 | EDIT |
用户角色表(`UserRoles`)
| user_id | role_id |
|---------|---------|
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 4 | 2 |
| 5 | 2 |
| 6 | 1 |
角色权限表(`RolePermissions`)
| role_id | permission_id |
|---------|---------------|
| 1 | 1 |
| 1 | 3 |
| 2 | 2 |
而学生课程成绩表也调整为如下:
学生课程成绩表:
| StudentID | CourseID | Grade |
|-----------|----------|-------|
| 1 | 101 | A |
| 1 | 102 | B |
| 2 | 101 | A |
| 2 | 103 | B |
课程讲师表:
| CourseID | Professor |
|----------|-----------|
| 101 | Smith |
| 102 | Jones |
| 103 | Brown |
第五范式
来看最后一个范式:第五范式,也被称为完美范式。它是为了消除连接依赖问题的,而连接依赖 是指数据可以通过多个表的连接来恢复的一种依赖关系。
这个概念似乎有些抽象,我查了一些资料之后,似乎依旧没能找到一个合适的例子来解释说明。但有一点可以确定的是,解决这类问题依旧是对表结构进行拆分,拆成粒度更细的表,解决数据冗余等问题。
更多的就不强行演示了,有知道的朋友可以留言。
反范式设计
经过上面的一些列案例演示,不难看到我们在满足每一个范式时,会伴随着数据表变得更加粒度化,也就是表越来越多了,这也会产生一些问题:
所以业务中我们会面临范式选择问题,根据自身的业务场景为其设计合适级别的范式,而并不一定要求100%遵循所有范式,这种设计思想就是反范式设计。如果反范式设计能够提升业务查询效率,减少连表查询的次数,那这个设计就是最优的。
总结
本篇总阐述了几大范式的概念及其设计思想,一定程度上遵循设计有助于我们设计出可维护、可扩展的表结构,但具体场景具体分析,通常情况下我们能够满足第一二三范式,权衡复杂性和扩展性的基础上,这个级别的范式是比较通用的。
对于打破范式设计的思想,如果对业务有利,也可以进行反范式设计。
以上的这些都是DB设计套路(框架),可以给我们业务开发中指定一个方向,需要使用时直接套上去即可。