[原创] 基础中的基础(三):理解数据库的几种键和几个范式

  在上学的时候,数据库是一门让我比较头大的课程。记得当时教材上净是一些晦涩难懂的语言,没有充足的实例来帮助理解。前一阵子在看《网络游戏服务器端编程》的过程中,突然对数据库范式有了一些感觉,在此总结一下,分享给大家。作者纯菜鸟,即使总结这些基础知识也难免有错,希望给位大牛不吝赐教,谢谢!

  键(关系键)以及数据库范式都是关系数据库的概念。所谓关系键,指的是一个表中的一个(或一组)属性,用来标识该表的每一行与另一个表产生联系

  数据库的”范式“,指的是设计数据库的规则。按照一定的规则设计出数据库的表和关系,能够避免在一些情况下的查询出错,并具有良好的结构。总的来说,随着范式等级的提高,数据表属性之间的依赖关系越来越小,数据冗余越来越低。但同时,数据关系变得更加复杂,访问一个具体数据的关系层次增加。所以像设计模式一样,不应盲目追求范式等级,应根据具体需求来选择范式。

 

  我们先来看一下几种常见的数据库关系键:

  1、超键(super key):能够唯一标识一条记录的属性或属性集。

    • 标识性:一个数据表的所有记录都具有不同的超键
    • 非空性:不能为空

  2、候选键(candidate key:能够唯一标识一条记录的最小属性集

    • 标识性:一个数据表的所有记录都具有不同的候选键
    • 最小性:候选键的任何子集都不能唯一标识一个记录
    • 非空性:不能为空
    • 候选键是没有多余属性的超键

  3、主键(主码、primary key)某个能够唯一标识一条记录的最小属性集

    • 唯一性:一个数据表只能有一个主键
    • 标识性:一个数据表的所有记录都具有不同的主键取值
    • 非空性:不能为空
    • 选取某个候选键为主键

  4、外键(foreign key):子数据表中出现的父数据表的主键,称为子数据表的外键。

  5、代理键:当不适合用任何一个候选键作为主键时(如数据太长等),添加一个没有实际意义的键作为主键,这个键就是代理键。(如常用的序号1、2、3)

  6、自然键:自然生活中唯一能够标识一条记录的键(如身份证)

 

  下面就来看一下常见的几种关系数据库范式吧。

 

一、第一范式(1NF)

  要求:

  •   每一个属性都不能再分割,都是原子项。

  第一范式是关系型数据表的基本要求,但是如何判断一个属性能否再分割呢?这没有统一的标准,需要依照需求确定。比如,我们要设计一个网络游戏后台所用的数据库,其中有一个数据表,记录有关于击杀怪物所获得的金钱:

编号

怪物名

掉落金钱

1

巨熊

100

   这个表格看上去并没有什么问题。每一个属性项都是”不可分割“的,所以符合第一范式。但是,如果我们希望把玩家击杀怪物之后获得的金钱分成两部分,一部分是固定收益,另一部分是一个随机的浮动收益(比如和玩家幸运值有关)。则这张表格中的”掉落金钱“项就不是”不可分割“了,也就不符合第一范式了。如果有这种需求,我们就可以把”掉落金钱“分割为”固定金钱“和”浮动金钱“两部分。如下所示:

编号

怪物名

固定金钱

浮动金钱

1

巨熊

80

20

  这样分割之后,使得每一项都不能再分割,从而使得数据表满足第一范式。 

  满足第一范式的数据表有什么好处呢?

  1. 1NF保证了数据库的每一列都是不同的。每一列的数据彼此没有任何交集。
  2. 这样做首先减少了数据的冗余,节省存储空间。如果不满足第一范式,一些数据项有可能包含相同的”子项“,造成存储空间的浪费。
  3. 其次,每一列没有重复的数据意味着不需要考虑数据更新的同步问题。不用担心在一列中更新了数据,还要在另一列做相应修改。
  4. 另外,每一列的数据不可再分,在某些情况下少了数据访问的层数,提高数据访问速度。

 

二、第二范式(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

  表中的依赖关系有:

  1. (玩家ID,房间ID)-> 房主ID
  2. 房主ID -> 房间ID
  3. (玩家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

 

  单独看这三个子表,我们可以得出以下结论:

  1. Alice和Bob对战过
  2. Alice在竞技场1和竞技场2都进行过对战
  3. Bob在竞技场1和竞技场2都进行过对战(结合第二、三个表)

 

  从这三个独立的结论,我们无法得知Alice和Bob究竟在那个竞技场进行的对战,也就是发生了信息丢失。所以上边那个表是符合5NF的。

 

  好了,6个范式都看完啦,简单总结一下:

范式等级

说明

1NF

每一列都是原子项,不可分割

2NF

非主键属性均完全依赖于主属性,消除部分依赖

3NF

所有非主键属性之间没有依赖关系,消除传递依赖

BCNF

所有属性均不传递依赖于任何候选键

4NF

表中不包含超过一个多值属性,消除多值依赖

5NF

将表拆分为二元关系,一定会损失信息

 

 

  感谢您看到这里!希望对您有一点帮助,欢迎批评和讨论! ^_^

 

  其他博客:

  基础中的基础(二):C/C++ 中 const 修饰符用法总结

  基础中的基础(一):简单排序算法总结(附代码)

你可能感兴趣的:(数据库)