//文章有来自它处的引用,文章内容有任何不妥之处请留言相告,不胜感激!!!
数据库的”范式“,指的是设计数据库的规则。按照一定的规则设计出数据库的表和关系,能够避免在一些情况下的查询出错,并具有良好的结构。总的来说,随着范式等级的提高,数据表属性之间的依赖关系越来越小,数据冗余越来越低。但同时,数据关系变得更加复杂,访问一个具体数据的关系层次增加。所以像设计模式一样,不应盲目追求范式等级,应根据具体需求来选择范式。
先来了解一下键的知识:
键(关系键)以及数据库范式都是关系数据库的概念。所谓关系键,指的是一个表中的一个(或一组)属性,用来标识该表的每一行或与另一个表产生联系。
键的定义:
超键(super key):在关系中能唯一标识元组的属性集称为关系模式的超键
候选键(candidate key):不含有多余属性的超键称为候选键
主键(primary key):用户选作元组标识的一个候选键程序主键
外键(foreign key)如果关系模式R1中的某属性集不是R1的主键,而是另一个关系R2的主键则该属性集是关系模式R1的外键。
结合实例的具体解释:
假设有如下两个表:
学生(学号,姓名,性别,身份证号,教师编号)
教师(教师编号,姓名,工资)
超键:由超键的定义可知,学生表中含有学号或者身份证号的任意组合都为此表的超键。如:(学号)、(学号,姓名)、(身份证号,性别)等。
候选键:候选键属于超键,它是最小的超键,就是说如果再去掉候选键中的任何一个属性它就不再是超键了。学生表中的候选键为:(学号)、(身份证号)。
主键:主键就是候选键里面的一个,是人为规定的,例如学生表中,我们通常会让“学号”做主键,教师表中让“教师编号”做主键。
外键:外键比较简单,学生表中的外键就是“教师编号”。外键主要是用来描述两个表的关系。
下面就来看一下常见的几种关系数据库范式吧。
一、第一范式(1NF)
要求:
- 每一个属性都不能再分割,都是原子项。
第一范式是关系型数据表的基本要求,(1NF是对属性的原子性约束,要求属性具有原子性,不可再分解)
例如:
name tel age
手机 座机
Josh 13612345678 021-9876543 22
Wang 13988776655 010-1234567 21
tel可以再分解为手机何电话,不具有原子性,不满足第一范式,做不出这样的表来。
修改方式:
name 手机 座机 age
Josh 13612345678 021-9876543 22
Wang 13988776655 010-1234567 21
这样分割之后,使得每一项都不能再分割,从而使得数据表满足第一范式。
满足第一范式的数据表有什么好处呢?
- 1NF保证了数据库的每一列都是不同的。每一列的数据彼此没有任何交集。
- 这样做首先减少了数据的冗余,节省存储空间。如果不满足第一范式,一些数据项有可能包含相同的”子项“,造成存储空间的浪费。
- 其次,每一列没有重复的数据意味着不需要考虑数据更新的同步问题。不用担心在一列中更新了数据,还要在另一列做相应修改。
- 另外,每一列的数据不可再分,在某些情况下减少了数据访问的层数,提高数据访问速度。
二、第二范式(2NF)就是在一个有唯一主键在表中保证每一行都具有唯一性。存在一个列被定义为唯一主键的表就是第二范式。
要求:
- 满足第一范式
- 非主键属性均完全依赖于主键
- 2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性,更通俗说有主键ID
非主键属性和主键可以有什么关系?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范式(所有属性均不传递依赖于任何候选键)
要求:
- 满足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的。