[原创] 基础中的基础(三):理解数据库的几种键和几个范式
在上学的时候,数据库是一门让我比较头大的课程。记得当时教材上净是一些晦涩难懂的语言,没有充足的实例来帮助理解。前一阵子在看《网络游戏服务器端编程》的过程中,突然对数据库范式有了一些感觉,在此总结一下,分享给大家。作者纯菜鸟,即使总结这些基础知识也难免有错,希望给位大牛不吝赐教,谢谢!
键(关系键)以及数据库范式都是关系数据库的概念。所谓关系键,指的是一个表中的一个(或一组)属性,用来标识该表的每一行或与另一个表产生联系。
数据库的”范式“,指的是设计数据库的规则。按照一定的规则设计出数据库的表和关系,能够避免在一些情况下的查询出错,并具有良好的结构。总的来说,随着范式等级的提高,数据表属性之间的依赖关系越来越小,数据冗余越来越低。但同时,数据关系变得更加复杂,访问一个具体数据的关系层次增加。所以像设计模式一样,不应盲目追求范式等级,应根据具体需求来选择范式。
我们先来看一下几种常见的数据库关系键:
1、超键(super key):能够唯一标识一条记录的属性或属性集。
-
- 标识性:一个数据表的所有记录都具有不同的超键
- 非空性:不能为空
2、候选键(candidate key):能够唯一标识一条记录的最小属性集
-
- 标识性:一个数据表的所有记录都具有不同的候选键
- 最小性:候选键的任何子集都不能唯一标识一个记录
- 非空性:不能为空
- 候选键是没有多余属性的超键
3、主键(主码、primary key):某个能够唯一标识一条记录的最小属性集
-
- 唯一性:一个数据表只能有一个主键
- 标识性:一个数据表的所有记录都具有不同的主键取值
- 非空性:不能为空
- 选取某个候选键为主键
4、外键(foreign key):子数据表中出现的父数据表的主键,称为子数据表的外键。
5、代理键:当不适合用任何一个候选键作为主键时(如数据太长等),添加一个没有实际意义的键作为主键,这个键就是代理键。(如常用的序号1、2、3)
6、自然键:自然生活中唯一能够标识一条记录的键(如身份证)
下面就来看一下常见的几种关系数据库范式吧。
一、第一范式(1NF)
要求:
- 每一个属性都不能再分割,都是原子项。
第一范式是关系型数据表的基本要求,但是如何判断一个属性能否再分割呢?这没有统一的标准,需要依照需求确定。比如,我们要设计一个网络游戏后台所用的数据库,其中有一个数据表,记录有关于击杀怪物所获得的金钱:
编号 |
怪物名 |
掉落金钱 |
1 |
巨熊 |
100 |
这个表格看上去并没有什么问题。每一个属性项都是”不可分割“的,所以符合第一范式。但是,如果我们希望把玩家击杀怪物之后获得的金钱分成两部分,一部分是固定收益,另一部分是一个随机的浮动收益(比如和玩家幸运值有关)。则这张表格中的”掉落金钱“项就不是”不可分割“了,也就不符合第一范式了。如果有这种需求,我们就可以把”掉落金钱“分割为”固定金钱“和”浮动金钱“两部分。如下所示:
编号 |
怪物名 |
固定金钱 |
浮动金钱 |
1 |
巨熊 |
80 |
20 |
这样分割之后,使得每一项都不能再分割,从而使得数据表满足第一范式。
满足第一范式的数据表有什么好处呢?
- 1NF保证了数据库的每一列都是不同的。每一列的数据彼此没有任何交集。
- 这样做首先减少了数据的冗余,节省存储空间。如果不满足第一范式,一些数据项有可能包含相同的”子项“,造成存储空间的浪费。
- 其次,每一列没有重复的数据意味着不需要考虑数据更新的同步问题。不用担心在一列中更新了数据,还要在另一列做相应修改。
- 另外,每一列的数据不可再分,在某些情况下减少了数据访问的层数,提高数据访问速度。
二、第二范式(2NF)
要求:
- 满足第一范式
- 非主键属性均完全依赖于主键
非主键属性和主键可以有什么关系?1、完全依赖。2、部分依赖。3、不依赖(没关系)。显然第三种情况下,这个属性就不应该放在这张数据表中。所以2NF要求非主键属性完全依赖于主键,就是在消除非主键属性对主键的部分函数依赖。既然是部分函数依赖,暗含着说主键是一个复合键(由多个属性组成的键)。如果某个非主键属性只和主键中的一部分有关(部分函数依赖),则不符合第二范式。举例,网络游戏的用户数据表:
玩家用户名 |
角色名 |
角色职业 |
上次登录时间 |
Alice |
superman |
wizard |
2013-11-4 |
如果我们的游戏允许一个玩家拥有多个角色,则在这张表中“玩家用户名”和“角色名”构成复合主键,唯一标识一条记录。表中的“角色职业”,与玩家用户名和角色名均相关,为完全依赖于主键。而“上次登录时间”仅和“玩家用户名”相关,而与角色名无关。所以“上次登录时间”部分函数依赖于主键。本关系不符合2NF。
要将上表转换为符合2NF的结构也很简单,只要把部分函数依赖的部分抽出来,组成新的表即可。如下所示:
玩家用户名 |
角色名 |
角色职业 |
Alice |
superman |
wizard |
玩家用户名 |
上次登录时间 |
Alice |
2013-11-4 |
符合2NF能给我们带来什么好处呢?2NF消除了属性对主键的部分函数依赖。
首先,2NF可以在一定程度上消除冗余,节省存储空间。
如果存在部分函数依赖,则可能存在数据冗余。在多条记录中,主键中的某一个属性可能是一样的,而如果有其他数据项函数依赖于这个不变的属性,则这些数据项也将是一样的。比如在上面例子中,在修改之前的表中,如果有多个角色名对应一个玩家用户名,则会有多条数据。它们具有一样的用户名和不同的角色名。由于上次登录时间仅依赖于玩家用户名,所以在这多条记录中,上次登录时间也都是相同的,造成了冗余。
其次,2NF简化了表的逻辑关系,使得表的结构更加清晰。
三、第三范式(3NF)
要求:
- 满足第一、二范式
- 所有非主键属性之间没有函数依赖关系
3NF在2NF的基础上,进一步消除非主键属性之间的函数依赖关系。实质上,也是消除非主键属性中的传递依赖。更进一步地说,如果两个数据表有关系。那么这两个数据表中的非主键属性必须是不同的。如果存在一个非主键属性A,存在于两张表中。则在某张表中,A依赖于外键,从而不符合3NF。比如网络游戏中拍卖行的数据,可以按照下面的表格进行存储:
玩家姓名 |
物品名 |
单价 |
数量 |
总金额 |
Alice |
治疗药剂 |
50 |
10 |
500 |
在这个表格中,“总金额”项可以通过“单价”和“数量”运算得出,存在函数依赖关系,不满足3NF。
要将这个表格修改为满足3NF的要求,只需要从表中删除“总金额”即可。在另外一些情况中,可以将函数依赖关系涉及到的项单独抽出来组成新的表,需要具体情况具体分析。
3NF的优点很明显,可以减少数据冗余,节省存储空间。既然存在函数依赖,某些数据项就能够通过其他数据项计算得出,很可能存在数据冗余。值得注意的是,在一些情况下,存在这种数据冗余的表格是有意义的。如果在表格中存储着某些运算的结果,我们在使用这些结果时就不用进行运算了,节省了运算时间,是一种“空间换时间”的做法。从这里也可以看出,应用范式并不能够保证最好的效果,需要根据应用需求进行合理取舍。
四、BC范式(boyce-codd范式,BCNF)
要求:
- 满足1NF、2NF、3NF
- 所有属性(包含主键属性和非键主属性)都不传递依赖于任何候选键
BC范式在3NF的基础上,要求主键属性也不能传递依赖于任何候选键。当主键是复合键是,主键的某个属性可能会依赖于某个候选键。此时,关系能够符合3NF,因为并不是“非主键”属性依赖于某个非主键属性。但此关系并不符合BC范式。例如,在以房间为组织方式的游戏中,我们记录某个玩家、房间和房主的关系。
房主ID |
房间ID |
玩家ID |
Alice |
123 |
Bob |
表中的依赖关系有:
- (玩家ID,房间ID)-> 房主ID
- 房主ID -> 房间ID
- (玩家ID,房主ID)-> 房间ID
同时,表中的候选键有(玩家ID,房间ID)、(玩家ID,房主ID)。比如,我们选择主键为(玩家ID,房间ID),那么,房间ID就是主键的一个属性。而在依赖关系2中,房间ID依赖于房主ID,房主ID是候选键(玩家ID,房主ID)的一个属性。那么,首先,由于房间ID不是候选键属性,所以此表并没有违反3NF。但是由于房间ID和房主ID存在依赖关系,所以满足“主键属性传递依赖于某个候选键”的条件,所以此表不符合BC范式。
要把上表修改为满足BC范式的形式,只要把它进行合理拆分即可。
房间ID |
玩家ID |
123 |
Bob |
房间ID |
房主ID |
123 |
Alice |
BC范式的好处是进一步消除了表中的依赖关系,减少了冗余。例如在上例中,如果我们采用未修改的版本,如果想要存储一个10个玩家(不含房主)的房间,就需要10条这样的记录才可以。
五、第四范式
要求:
- 满足1NF、2NF、3NF
- 表中不能包含一个实体的两个或多个多值属性
所谓多值属性,指的是某个属性可以包含多个值。这个属性的(多个)取值,被另一个属性决定。也就是说,一旦确定了某个属性,另一个属性的多个取值就一起确定了。第四范式在第三范式的基础上,消除多值依赖。所谓多值依赖,指的是一组值(多值属性)依赖于另一个属性。函数依赖是一对一的关系,多值依赖是一对多的关系。这个理解起来我感觉有点别扭,可能我的理解也有偏差,说出来和大家一起探讨一下。
比如,我们要在数据库中保存玩家的角色技能信息,这里我们允许一个玩家具有多个角色,一个角色具有多个技能:
玩家ID |
角色名 |
技能 |
Alice |
superman |
Fire ball |
首先,这个表只有一个候选键(玩家ID、角色名、技能)。所以肯定符合3NF。进一步观察一下,玩家ID是一个单值属性。角色名就是一个多值属性了,因为一个玩家ID可以对应多个角色名。角色名在表中看起来是一项,这是由于受制于具体数据库提供的功能。逻辑上,我们拿到一个玩家ID,可以确定的是,这个玩家具有某些角色,是一个一对多的关系,是多值依赖。角色名是一个多值属性。同样的,一个角色也对应着多个技能,这也是多值依赖。技能也是一个多值属性。显然,这个表并不符合4NF。
这个表有什么问题呢?
首先,数据冗余大,如果一个玩家有好几个具有Fire Ball技能的角色,这个技能项就要重复保存几次。
其次,增、删、改都比较复杂,比如我们要删除Fire Ball技能,那么,我们要删除这个玩家所有具有Fire Ball技能的表项。
要将上表修改为符合4NF的表,只需要将多值依赖进行合理映射即可:
玩家ID |
角色名 |
Alice |
superman |
角色名 |
技能 |
superman |
Fire ball |
这两个表都符合4NF。
可以看出,4NF的使用可以降低数据冗余,并且减少数据处理复杂度。
六、第五范式
要求:
- 满足1NF、2NF、3NF、4NF
- 如果将表中的多元关系分解一个一个的二元关系,一定会丢失信息
第五范式在4NF的基础上,进一步消除依赖。第五范式的要求明,如果不用这个表就不能正确说明数据之间的联系。所以符合5NF的表已经没有任何多余依赖的存在了。所以第五范式是一个比较理想的范式。比如我们存储玩家对战和其发生地点:
玩家1 |
玩家2 |
对战地点 |
Alice |
Lisa |
竞技场1 |
Alice |
Bob |
竞技场2 |
Bob |
Lisa |
竞技场1 |
现在,我们把它拆分成三个二元关系:
玩家1 |
玩家2 |
Alice |
Lisa |
Alice |
Bob |
Bob |
Lisa |
玩家1 |
对战地点 |
Alice |
竞技场1 |
Alice |
竞技场2 |
Bob |
竞技场1 |
玩家2 |
对战地点 |
Lisa |
竞技场1 |
Bob |
竞技场2 |
Lisa |
竞技场1 |
单独看这三个子表,我们可以得出以下结论:
- Alice和Bob对战过
- Alice在竞技场1和竞技场2都进行过对战
- Bob在竞技场1和竞技场2都进行过对战(结合第二、三个表)
从这三个独立的结论,我们无法得知Alice和Bob究竟在那个竞技场进行的对战,也就是发生了信息丢失。所以上边那个表是符合5NF的。
好了,6个范式都看完啦,简单总结一下:
范式等级 |
说明 |
1NF |
每一列都是原子项,不可分割 |
2NF |
非主键属性均完全依赖于主属性,消除部分依赖 |
3NF |
所有非主键属性之间没有依赖关系,消除传递依赖 |
BCNF |
所有属性均不传递依赖于任何候选键 |
4NF |
表中不包含超过一个多值属性,消除多值依赖 |
5NF |
将表拆分为二元关系,一定会损失信息 |
感谢您看到这里!希望对您有一点帮助,欢迎批评和讨论! ^_^
其他博客: