数据库三大范式(考试必备)

发布时间 2023-06-20 10:47:05作者: 蚂蚁小哥

一:什么是数据库范式

  设计关系数据库时,要遵从不同的规范要求,设计出合理的关系型数据库,这些不同的规范要求被称为不同的范式,各种范式呈递次规范,越高的范式数据库冗余越小。所以要想设计一个结构合理的关系型数据库,必须满足一定的范式。其实范式的英文名称是Normal Form,简称NF。它是英国人E.F.Codd在上个世纪70年代提出关系数据库模型后总结出来的。范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。

1:范式的作用

  使结构更合理和规范化,消除存储异常,使数据的冗余减少。便于插入、删除和更新。其实减少数据的冗余,就是尽量让每个数据只出现一次,要获取指定的字段数据时就通过JOIN的方式来拼接获取最终的数据。
  所以范式的作用就是用来解决数据冗余增删改的异常,要判断一个数据库设计的是否存在问题就从这两点判断。

2:范式都包括哪些

  目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯科德范式(BCNF)、第四范式(4NF)、第五范式(5NF,又称完美范式);数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,满足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步满足更多规范要求的称为第二范式(2NF),其余范式以次类推。一般来说,在关系型数据库设计中,最高也就遵循到BCNF,普遍还是3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化

3:基础数据准备

-- 创建数据库和没有任何约束的表信息
CREATE DATABASE IF NOT EXISTS demo_nf CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE demo_nf;
CREATE TABLE IF NOT EXISTS student(
    sno INT COMMENT '学号',
    sname VARCHAR(10) COMMENT '姓名',
    sdept VARCHAR(10) COMMENT '专业',
    mname VARCHAR(5) COMMENT '系主任姓名',
    cno INT COMMENT '选课编号',
    grade DECIMAL(5,2) COMMENT '成绩'
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 添加数据
INSERT INTO demo_nf.student VALUES
(1001,'王生安','信息与计算机科学','李主任',2206,76.5),(1001,'王生安','信息与计算机科学','李主任',2207,80.0),
(1002,'钱勤堃','分子科学与工程','王主任',2335,60.0),(1003,'周卓浩','地球信息科学与技术','崔主任',3222,89.5),
(1003,'周卓浩','地球信息科学与技术','崔主任',3223,56.5),(1003,'周卓浩','地球信息科学与技术','崔主任',3224,32.5),
(1004,'张灶冲','数学与应用科学','鲍主任',4125,66.5),(1004,'张灶冲','数学与应用科学','鲍主任',4126,70.0),
(1004,'张灶冲','数学与应用科学','鲍主任',4127,64);
-- 课程编号对于的课程(这里我就没建立关联表了,后面用不上)
2206 -> '计算机组成原理' ; 2207 -> '算法与数据结构'
2335 -> '无机化学'
3222 -> '构造地质学' ; 3223 -> '空间测地学' ; 3224 -> '地质学'
4125 -> '高等代数' ; 4126 -> '概率统计' ; 4127 -> '数学分析'
说明:从上面的关系可以看出
    知道学号可以查询出当前学生的具体:姓名(sname)、专业(sdept)、系主任姓名(mname)
    知道学号号选课编号可以查询当前学生的具体:成绩(grade)
数据查询信息:
    SELECT * FROM demo_nf.student;
    +------+-----------+-----------------------------+-----------+------+-------+
    | sno  | sname     | sdept                       | mname     | cno  | grade |
    +------+-----------+-----------------------------+-----------+------+-------+
    | 1001 | 王生安    | 信息与计算机科学               | 李主任     | 2206 | 76.50 |
    | 1001 | 王生安    | 信息与计算机科学               | 李主任     | 2207 | 80.00 |
    | 1002 | 钱勤堃    | 分子科学与工程                 | 王主任     | 2335 | 60.00 |
    | 1003 | 周卓浩    | 地球信息科学与技术             | 崔主任     | 3222  | 89.50 |
    | 1003 | 周卓浩    | 地球信息科学与技术             | 崔主任     | 3223 | 56.50  |
    | 1003 | 周卓浩    | 地球信息科学与技术             | 崔主任     | 3224 | 32.50  |
    | 1004 | 张灶冲    | 数学与应用科学                 | 鲍主任     | 4125 | 66.50 |
    | 1004 | 张灶冲    | 数学与应用科学                 | 鲍主任     | 4126 | 70.00 |
    | 1004 | 张灶冲    | 数学与应用科学                 | 鲍主任     | 4127 | 64.00 |
    +------+-----------+-----------------------------+-----------+------+-------+
注意:在实际开发中一定不要以这种方式创建数据库,这案例只是为了引出范式的概念和优化方式

二:关系数据理论

1:关系模式

其实一个关系模式由五部分组成,是一个五元组:R(U, D, DOM, F)
  (1) R: 关系名R是符号化的元组语义;
  (2) U: 一组属性(就是表的列);
  (3) D: 属性组U中属性所来自的域;
  (4) DOM: 属性到域的映射;
  (5) F: F就是属性组U上的一组数据依赖(就是表的列存在的依赖关系);
由于 D、DOM 对模式设计关系不大,因此可以把关系模式看作是一个三元组:R<U,F>,当且仅当U上的一个关系r满足F时,r称为关系
模式R<U,F>的一个关系。其实作为一张二维表,对它有一个最起码的要求:每一个分量必须是不可分的数据项(后面会说范式)。满足了
这个条件的关系模式就属于第一范式(1NF)。

2:数据依赖

  其实数据依赖一般存在两种,分别是函数依赖(Functional Dependency,FD)多值依赖(Multivalued Dependency,MVD)

什么是数据依赖呢?大概可以分为如下几点:
    ①:它是一个关系内部属性与属性之间的一种约束关系;其实就是通过属性间值的相等与否体现出来的数据间相互联系
    ②:是现实世界属性间相互联系的抽象
    ③:是数据内在的性质
    ④:是语义的体现
从上面的介绍难免有点晦涩难懂,举个例子:
    比如描述一个学生的关系,可以有学号(sno)、姓名(sname)、系名(sdept)等几个属性。由于一个学号只对应一个学生,一个学生只在一个
    系学习。因而当“学号”值确定之后,学生的姓名及所在系的值也就唯一地确定了。属性间的这种依赖关系类似于数学中的函数y=f(x),自变
    量x确定之后,相应的函数值y也就唯一确定了。类似的有sname=f(sno),sdept=f(sno),即sno函数决定sname,sno函数决定sdept,
    或者说sname和sdept函数依赖于sno,记作sno→sname, sno→sdept。 其实上面的例子就像我们平时写编程的函数或方法一样,定义一
    个fun(type e)函数一样,我们传入e的值(如上面的x)可以获得指定的y。
    例:针对上面的介绍建立一个描述学校教务的数据库,该数据库涉及的对象包括学生的学号(sno)、姓名(sname)、所在系(sdept)、系主任
        姓名(mname)、课程号(cno)和成绩(grade)。假设用一个单一的关系模式student来表示,则该关系模式的属性集合为::
            U = {sno, sname, sdept, mname, cno, grade}
    现实世界的已知事实告诉我们:
        (1) 一个系有若干学生,但一个学生有他对应的姓名,并且还只属于一个系
        (2) 一个系只有一名(正职)负责人
        (3) 一个学生可以选修多门课程,每门课程有若干学生选修
        (4) 每个学生学习每一门课程有一个成绩
    于是得到属性组U上的一组函数依赖F:F={sno→sdept, sno→sname, sno->mname, sdept→mname, (sno, cno)→grade}如下图所示:
    如果只考虑函数依赖这一种数据依赖,我们就得到了一个描述学生的关系模式:student<U, F>。
    下表是某一时刻关系模式student的一个实例,即数据表(但是,这个关系模式存在问题):
以上关系模式存在的问题:
    就像我之前说的,判断一个表设计的好不好就从“数据冗余+增删改异常”来判断为什么设计的表不好
    ①:数据冗余太大
        比如,每一个系的系主任姓名重复出现,重复次数与该系所有学生的所有课程成绩出现次数相同,这将浪费大量存储空间。
        (合理的方式应该把'系主任姓名'抽离出去,数据的获取则通过表与表的JOIN方式获取)
    ②:更新异常(Update Anomalies)
        由于数据冗余,当更新数据库中的数据时,系统要付出很大的代价来维护数据库的完整性,否则会面临数据不一致的危险。
        比如,某系更换系主任后,必须修改与该系学生有关的每一个元组。
    ③:插入异常(Insertion Anomalies)
        如果一个系刚成立,尚无学生,就无法把这个系及其系主任的信息存入数据库。
    ④:删除异常(Deletion Anomalies)
        如果某个系的学生全部毕业了,在删除该系学生信息的同时,把这个系及其系主任的信息也丢掉了。
鉴于存在以上种种问题,我们可以得出这样的结论(后面会详细说明怎么拆分)
    student关系模式不是一个好的模式。一个"好"模式应当不会发生插入、删除、更新异常,数据冗余应尽可能少。为什么会发生这些问题?
    这是因为这个模式中的函数依赖存在某些不好的性质。假如把这个单一的模式改造一下,分成3个关系模式(即做成3张表):
    s(sno, sname, sdept, sno→sdept, sno->sname);
    sc(sno, cno, grade, (sno, cno)→grade);
    dept(sdept, mname, sdept→mname)
    这三个模式都不会发生插入、删除异常的毛病,数据的冗余也得到了控制。

三:函数依赖

1:什么是函数依赖

定义:设R(U)是一个属性集U上的关系模式,X和Y是U的子集。若对于R(U)的任意一个可能的关系r,r中不可能存在两个元组在X上的属性值相等,而在Y上的属性值不等,则称“X函数确定Y"或“Y函数依赖于X”,记作X->Y。

简单说就是,在一个表里面,属性X(主属性)可以映射到属性Y(非主属性),也就是说知道了X就能确定Y,称X为决定因素。
如一张表的属性信息:U = {sno, sname, ssex, sage, sdept, mname}
    以数学中的函数y=f(x)来说,自变量x确定之后,相应的函数值y也就唯一确定了。
    属性X包含:学号(sno)
    属性Y包含:姓名(sname)、性别(ssex)、年龄(sage)、系名(sdept)、系主任姓名(mname)
        通过函数依赖我们可以得知:
            sname=f(sno) 通过sno可以得出sname;可以记作 sno->sname
            列出其它:sno->ssex、 sno->sage、 sno->sdept、 sno->mname
        假设sname的姓名不可重复,被当作了属性X,则会再多出一些函数依赖关系:
            sname->ssex、 sname->sage、 sname->sdept、 sname->mname
        若存在多个属性X则会发生互相依赖:
            sname <--> sno
        那属性Y之间存在依赖关系吗?
            其实Y(非主属性)也会存在依赖关系(如:函数传递依赖),但是设计一张好的表尽量不要存在依赖关系。
            如上面的关系:ssex 无法确定 sage, sage 无法确定 ssex
总结:若X->Y,并且Y->X,则记为X<-->Y。若Y不函数依赖于X,则记为 X无法确定Y。

2:平凡和非平凡函数依赖

平凡函数依赖:X可以决定Y,但Y属于X,则称X->Y是平凡函数依赖。
    如一张表的属性信息:U = {sno, sname, ssex, sage}
        可以得出 sno=f(sno) ,就是sno->sno
    再如一张表的属性信息:U = {sno, sname, ssex, cno, grade}
        可以得出 sno=f(sno,cno) ,就是(sno,con)->sno或(sno,con)->cno
    所以对于任一关系模式,平凡函数依赖都是必然成立的,它不反映新的语义。若不特别声明,我们总是讨论非平凡函数依赖。
非平凡函数依赖:X可以决定Y,但Y不属于X,则称X->Y是非平凡函数依赖。
    如一张表的属性信息:U = {sno, sname, ssex, sage}
        可以得出 sname=f(sno) ,就是sno->sname
    再如一张表的属性信息:U = {sno, cno, grade}
        可以得出 grade=f(sno,cno) ,就是(sno,con)->grade
总结:
    若X->Y,则X称为这个函数依赖的决定因素(Determinant)
    若X->Y,Y->X,则记作X<-->Y。(X和Y互相决定)
    若Y不函数依赖于X,则记作X不决定Y。

3:完全和部分函数依赖

完全函数依赖:在R(U)中,如果X决定Y,并且X存在多个,那么在获取Y时必须依赖全部的X才可以确定Y,则称Y对X是完全函数依赖
    如一张表的属性信息:U = {sno, sname, ssex, cno, grade}
        若要得出具体的grade属性则必须的依赖关系是:(sno,cno)->grade
        因为我想知道某位学生的某一门课的成绩grade,那我必须得同时知道他的学号sno和课程号cno。
        但如果我只知道一部分信息,如sno或者cno是不行的,因为此时称y[grade]完全依赖于x[sno,cno]。
部分函数依赖在R(U)中,若X决定Y,并且X存在多个,那么获取Y时不必需要依赖全部的X才可以确定Y,则称Y对X是部分函数依赖
    如一张表的属性信息:U = {sno, sname, ssex, cno, grade}
        如果我想知道某位学生的姓名sname,那我知道他的学号sno就可以了。
        也就是说y[sname]只函数依赖于x[sno,cno]中的子集x[sno],此时称y部分函数依赖于x。
注:若一张表里面只有一个X,那么将不会构成部分函数依赖

4:传递函数依赖

传递函数依赖:在R(U)中,Z函数依赖于Y,且Y函数依赖于X,并且Y不函数依赖于X,X不函数依赖于Y,那么我们就称Z传递函数依赖于X
如一张表的属性信息:U = {sno, sdept, mname}
    如果我知道了一个学生的学号sno,那我就能知道他所在的系sdept。(因为理论上一个学生只属于一个系)
    如果我知道了某一个系sdept,那么我就能知道这个系的系主任的姓名mname。(一个系只有一个正的系主任)
    也就是说,我知道了一个学生的学号sno,其实我就知道了他所在系的系主任的姓名mname。但这个过程中,
    它们是不存在直接函数依赖的,我需要通过系名称sdept作为一个桥梁去把二者联系起来的。
    可以得出:sno->sdept->mnane
但是需要注意的是:如一张表的属性信息:U = {sno, sname, sdept}
    如果Y->X,即X<-->Y, 则Z直接依赖于X,而不是传递函数依赖;
    虽然我可以sno->sname->sdept;但是sno->sdept也是可以确定的

四:数据库码的说明

  码是数据库系统中的基本概念,所谓码就是能唯一标识实体的属性,比如学生表中的id就能够唯一标识出一条具体记录的属性字段,除了id字段,其实学号字段或身份证字段都能标识出唯一一条记录的都可以称作为码,但是如学生性别和地址字段就无法唯一标识出具体的一条实体记录,所以这种字段无法被称为码;其实码是整个实体集的性质,而不是单个实体的性质。它包括超码、候选码和主码等。(其实码就和我们日常说的键是一回事)

下面以这个几个表字段说明: U = {sno, scardid, sname, sage, sdept}
超码:是一个或多个属性的集合,这些属性可以让我们在一个实体集中唯一地表示一个实体。如果K是一个超码,那么所有包含K的集合也是
  超码。可以看出sno(学号),scardid(身份证号)可以唯一标识实体的属性字段,那么它就被称为码,其实也可以被称为超码
还要知道多个属性字段里面包含一个超码,那么也会变为超码(可以理解超集,里面包含一个码的超集),如: (sno)、(scardid)、(sno, scardid)都是超码 (sno,sname,sage)是超码,因为它是一个包含sno超码的集合;但是这个不是码 (sno, scardid, sname, sage)是超码 (sname, sage)不是超码 候选码:能够唯一标识一条记录的最小属性集。若关系中的一个属性或属性组的值能够唯一地标识一个元组(一条记录),且它的真子集不能
  唯一的标识一个元组,则称这个属性或属性组做候选码。
比如上面的sno(学号),scardid(身份证号)都可以被称为最小的候选码;但注意的是(sno, scardid)组合起来将不在是候选码, 而是一个由多个属性组成并包含一个码的超码; 假设通过(sname, sage, sdept)三个属性可以定位一条记录,且少一个属性都无法定位具体的一条记录,则这个也被称为最小 的候选码,因为真子集不能唯一的标识一个元组; 主码:主码其实指的是主键。主关键字(PRIMARY KEY)是表中的一个或多个字段,它的值用于唯一地标识表中的某一条记录。在两个表的关
  系中,主关键字用来在一个表中引用来自于另一个表中的特定记录。主关键字是一种唯一关键字,表定义的一部分。一个表的主键可以由
  多个关键字共同组成,并且主关键字的列不能包含空值。主关键字是可选的,并且可在CREATE TABLE或ALTER TABLE语句中定义。
说白了就是被数据库设计者选中的,用来在同一个实体集中区分不同实体的候选码。选择的是那些从不或极少变化的候选码作为主码, 从上面的三个候选码中sno(学号)、scardid(身份证号)、(sname, sage, sdept)组合候选码,最合适做主码的就是sno(学号)和 scardid(身份证号)了,因为这个属性字段极少被修改 全码:关系模型中的所有属性组组成该关系模式的候选码,称为全码。即所有属性当作一个码。若关系中只有一个候选码,且这个候选码中包
  含全部属性则该候选码为全码。
说白了就是表里面的全部属性都是码,那么就被称为全码 外码:可以被称为外键(FOREIGN KEY),比如数据表R1中的某属性集不是R1的主码,而是另一个数据表R2的主码,那么这个属性集就是数据 表R1的外码。 主属性:包含在任一候选码中的属性称主属性。简单来说,主属性是候选码所有属性的并集 非主属性:不包含在候选码中的属性被称为非主属性。非主属性是相对于主属性相反的属性来定义的。 主码和候选码的区别:主码可以唯一标识,候选码是可以作为主码的码,主码一定是候选码的子集,但候选码不一定是主码。

五:第一范式(1st NF)

  一个关系模式R里的所有属性都是不可分的基本数据项,就是说要确保数据表中每个字段的值必须具有原子性,每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。

示例1:
    假设一家公司要存储员工的姓名和联系方式。它就创建一个如下表:
        +-----+--------+----------+--------------+------------------------+
        | uid | emp_id | emp_name | emp_address  | emp_mobile             |
        +-----+--------+----------+--------------+------------------------+
        |   1 |   1001 | 张三      | 安徽六安      | 17681111145            |
        |   2 |   1002 | 李四      | 安徽合肥      | 15589874589,13658957488|
        |   3 |   1003 | 王五      | 安徽安庆      | 14687485987            |
        +-----+--------+----------+--------------+------------------------+
    其实这张表就不符合1NF,因为这张表不满足“同一列中不能有多个值”;其中emp_mobile字段的第二行发现李四存了两个手机号。
    应该改为如下方式(把上面存在一列多个值的数据拆分为2行数据):
        +-----+--------+----------+--------------+-------------+
        | uid | emp_id | emp_name | emp_address  | emp_mobile  |
        +-----+--------+----------+--------------+-------------+
        |   1 |   1001 | 张三      | 安徽六安      | 17681111145 |
        |   2 |   1002 | 李四      | 安徽合肥      | 15589874589 |
        |   3 |   1002 | 李四      | 安徽合肥      | 13658957488 |
        |   4 |   1003 | 王五      | 安徽安庆      | 14687485987 |
        +-----+--------+----------+--------------+-------------+
示例2:
    下面的student表就不符合第一范式:
    CREATE TABLE student (
        id int(11) NOT NULL PRIMARY KEY         COMMENT '主键ID',
        sno int(11) DEFAULT NULL                COMMENT '学号',
        student_info varchar(255) DEFAULT NULL  COMMENT '学生信息(包含姓名,性别,年龄)')
    student_info字段为学生信息,它还可以拆分成更小粒度的字段,不符合数据库设计对第一范式要求的“每个字段的值必须具有原子性”。
    将student_info拆分后如下:
    CREATE TABLE student (
        id int(11) NOT NULL PRIMARY KEY         COMMENT '主键ID',
        sno int(11) DEFAULT NULL                COMMENT '学号',
        sname varchar DEFAULT NULL              COMMENT '姓名',
        ssex varchar DEFAULT NULL               COMMENT '性别',
        sage int DEFAULT NULL                   COMMENT '年龄')
举例3:
属性的原则性是主观的,不是说字段必须要原则性 例如有些人设计student表中的姓名字段都是有好几种方式: 使用1个字段的(fullname),使用2个字段的(firstname、lastname),使用3个字段的(firstname、middlename、lastname); 为什么会有这么多种方式的属性设计呢?其实都是取决于业务和应用程序,如果应用程序需要分别处理student表中的姓和名,经常对”姓“ 进行查询,那就则有必要把它们分开。否则,不需要。 比如有的表address字段存储的是这种格式:“xx省xx市xx区xx镇”;但是业务或者应用程序的需要,把省、市、区、镇这拆分为4个不同的 字段存储都是有可能的

六:第二范式(2st NF)

  第二范式要求在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的(就是要有主属性)。非主属性必须完全依赖于候选码(在1NF基础上消除非主属性对主码的部分函依赖)。

如一张表U = {p1,p2,np1,np2}
    其中主属性有(p1,p2)非主属性(np1,np2)
    那么候选码和主码都为(p1,p2);这时需要满足第二范式则np1字段和np2字段必须完全依赖(p1,p2),缺一个码都不行
示例1:
    如学生表 student = ( U={sno, cno, grade} ) 关系中;(sno, cno)为主码,并且这个主码可以决定grade(成绩)信息,
    但是一个sno(学号)字段不能决定成绩,而且一个cno(课程号)也不能决定成绩,只有(sno, cno)两个字段一起才可以决定当前
    表上面的grade(成绩)信息,这就是满足了第二范式函数的完全依赖关系,即非主属性必须完全依赖于候选码(主码是候选码的子集)
示例2:
    如学生表 student = ( U={sno(学号), sname(姓名), sdept(专业), mname(系主任名称), cno(课程号), grade(成绩)} )
    这里的候选码和主码都为(sno(学号), cno(课程号));我们可以通过候选码(或主码)来决定如下的关系:
        (sno,cno) -> (sname, sdept, mname, grade)
    但是上面的关系中,主属性和非主属性不满足第二范式,因为数据表中的主属性子集之间还存在着如下对应关系:
        (sno) -> (sname, sdept, mname),这种就违背了第二范式的“非主属性对主码的完全函依赖”
    对于非主属性来说,并非完全依赖候选码。这样会产生“数据冗余+增删改异常”,这可以参考上面的2.2介绍的数据依赖问题。
    为了避免上面的问题出现,我们就需要进行拆分表结构来满足第二范式,拆分方式如下:
        成绩表 = (U={sno, cno, grade})
        学生表 = (U={sno, sname, sdept, mname})
        其实拆分有个速记规则:”谁跟你好,复制自己,把它带走“
            “谁跟你好”:站在主属性的视角看,看看非主属性哪个和我构成完全函数依赖;
            “复制自己”:站在刚才的主属性视角,把自己属性复制一份出去构建一张表如t1表;
            “把它带走”:站在之的非主属性上,把那些和我构成完全函数依赖的非主属性带走放到我最新创建的t1表里;
        如上面的sno主属性和非主属性字段构成了完全函数依赖,那就要复制sno字段,并且把和我构成完全函数依赖的非主属性带走构建新表
总结:1NF告诉我们字段属性需要是原子性的,而2NF告诉我们一张表就是一个独立的对象,一张表只表达一个意思。所以第二范式(2NF)要
  求实体的属性完全函数依赖于主码
。如果存在不完全函数依赖,那么这个属性和主码的这一部分应该分离出来形成一个新的实体,新实
  体与未分离的实体之间是一对多的关系。

七:第三范式(3st NF)

  第三范式是在第二范式的基础上,确保数据表中的每一个非主属性字段都要和主码直接相关,也就是说,要求数据表中的所有非主属性字段不能依赖于其它非主属性字段(就是在2NF基础上消除传递依赖)。比如不能存在非主属性A依赖于非主属性B,非主属性B依赖于主属性(主码)C的情况,即存在“A->B->C”的决定关系;通俗地讲,该规则的意思是说所有非主属性之间不能有依赖关系,必须相互独立。这里的主属性可以拓展为候选码。

示例1:
    现在有2张表分别为:
        部门信息表 = (U = { 部门编号, 部门名称, 部门简介 })
        员工信息表 = (U = { 员工编号, 姓名, 部门编号})
    其实上面的这2张表就符合第三范式,部门表的部门编号为主码,其它非主属性依赖于主码;员工表的员工编号为主码,其它的非主属性也是
    依赖于员工编号的这个主码;
    但是若要给员工信息表增加一个”部门名称“字段就会破坏第三范式规范,因为非主属性之间不能存在依赖关系
        以员工信息表里来说:部门名称->部门编号->员工编号;这就会造成传递依赖
示例2:
     如学生表 student = ( U={sno(学号), sname(姓名), sdept(专业), mname(系主任名称), cno(课程号), grade(成绩)} )
     我们拆分了满足范式二的要求如下:
        成绩表 = (U={sno, cno, grade})
        学生表 = (U={sno, sname, sdept, mname})
    但是要满足第三范式则需要“非主属性字段不能依赖于其它非主属性字段”;可以看出学生表中的非主属性(sname, sdept, mname)
    里面的(sdept, mname)构成了传递依赖,其中非主属性sdept(专业)依赖于sno(学号), 而非主属性mname(系主任名称)则可以依赖于
    非主属性sdept(专业),因为通过专业可以找到具体的专业里的系主任,所以需要如下修改,拆分学生表达到第三范式:
        成绩表 = (U={sno, cno, grade})
        学生表 = (U={sno, sname, sdept})
        专业表 = (U={sdept, mname})
    其实拆分有个速记规则:”谁跟你好,复制自己,把它带走“;因为mname依赖sdept,所以以sdept为主码创建一张表,把依赖自己的复制
    到新的表,然后剔除学生表的mname字段

八:范式1~3总结

关于数据表的设计,有三个范式需要遵循:
    第一范式:
        数据库的每一列都是不可分割的原子数据项,不可再分的最小数据单元,而不能是集合、数组、多条记录等非原子数据项。
    第二范式:
        确保每列属性都和主键完全依赖,尤其在复合主键的情况下,非主键部分不应该依赖于部分主键。
    第三范式:
        确保每列都和主键列直接相关,而不是间接相关(传递依赖)
范式的优点:数据的标准化有助于消除数据库中的数据冗余,第三范式(3NF) 通常被认为在性能、扩展性和数据完整性方面达到了最好的平衡。
范式的缺点:范式的使用,可能降低查询的效率。因为范式等级越高,设计出来的数据表就越多、越精细,数据的冗余度就越低,进行数据查询
        的时候就可能需要关联多张表,这不但代价昂贵,也可能使一些索引策略无效。

范式只是提出了设计的标准,实际上设计数据表时,未必一定要符合这些标准。开发中,我们会出现为了性能和读取效率违反范式化的原则,
通过增加少量的冗余或重复的数据来提高数据库的读性能,减少关联查询,JOIN表的次数,实现空间换取时间的目的。因此在实际的设计过程
中要理论结合实际,灵活运用。

总结:范式本身没有优劣之分,只有适用场景不同。没有完美的设计,只有合适的设计,我们在数据表的设计中,还需要根据需求将范式和反
    范式混合使用。

九:反范式化

1:概述

  有的时候不能一味的按照规范要求设计数据表,因为有的数据看似冗余,其实对业务来说十分重要。这个时候我们就要遵循业务优先的原则,首先满足业务需求,再尽量减少冗余。如果数据库中的数据量比较大,系统的UV(独立访客)和PV(网站浏览量)访问频次比较高,则完全按照MySQL的三大范式设计数据表,读数据时会产生大量的关联查询,在一定程度上会影响数据库的读性能。如果我们想对查询效率进行优化,反范式优化也是一种优化思路。此时,可以通过在数据表中增加冗余字段来提高数据库的读性能。

规范化 VS 性能:
    ①:为满足某种商业目标,数据库性能比规范化数据库更重要
    ②:在数据规范化的同时,要综合考虑数据库的性能I
    ③:通过在给定的表中添加额外的字段,以大量减少需要从关联查询得到的信息所需的时间
    ④:通过在给定的表中插入计算列,以方便查询
示例:
    employees = ( U={employee_id(编号), ename(姓名), essex(性别), department_id(部门id,其实也是外键)} )
    departments = ( U={department_id(编号), department_name(部门名称), dmsg(部门信息)} )
    比如现在有两张表,员工的信息存储在employees表中,部门信息存储在departments表中。通过employees表中的department_id字段
    与departments表建立关联关系。如果要查询一个员工所在部门的名称:
        SELECT e.employee_id, d.department_name
        FROM employees e JOIN debartments d USING(department_id);
    假设数据量都已经达到50多万以上,而且还要经常需要进行这种的SQL查询操作,其中连接查询就会浪费很多时间;那么为什么不考虑在
    employees表中添加一个冗余字段“department_name”呢,这样每次查询就不需要进行连接操作了(这种就是典型的反范式化);
    改造的表(添加一个冗余字段):
    employees = ( U={employee_id(编号), ename(姓名), essex(性别), department_name(部门名称),
            department_id(部门id,其实也是外键)} )
    departments = ( U={department_id(编号), department_name(部门名称), dmsg(部门信息)} )

2:反范式化的问题及适用场景

反范式可以通过空间换时间,提升查询的效率,但是反范式也会带来一些新问题:
    ①:存储空间变大了
    ②:一个表中字段做了修改,另一个表中冗余的字段也需要做同步修改,否则数据不一致
    ③:若采用存储过程来支持数据的更新、删除等额外操作,如果更新频繁,会非常消耗系统资源
    ④:在数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂
反范式的适用场景:
    当冗余信息有价值或者能大幅度提高查询效率的时候,我们才会采取反范式的优化。
    Ⅰ:增加冗余字段的建议
        增加冗余字段一定要符合如下两个条件。只有满足这两个条件,才可以考虑增加冗余字段。
            这个冗余字段不需要经常进行修改;
            这个冗余字段查询的时候不可或缺。
    Ⅱ:历史快照、历史数据的需要
        在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都
        属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。
        反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。
        这时适当允许数据的冗余度,更方便进行数据分析。
            简单总结数据仓库和数据库在使用上的区别:
                1.数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据;
                2.数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据;
                3.数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。

十:巴斯-科德范式(BCNF)

  在第1~3范式的基础上进行了改进,提出了巴斯范式(BCNF),也叫巴斯-科德范式(Boyce-Codd Normal Form);BCNF其实并没有新的设计规范加入,只是对第三范式中设计规范要求更强,使得数据库冗余度更小。所以,称为是修正的第三范式,或扩充的第三范式,BCNF不被称为第四范式。
  BCNF主要就是消除主属性对码(单码或复合码)的部分函数依赖和传递函数依赖;
  其实我们可以看出,第一范式解决了属性的原子性问题,第二范式解决了非主属性对码的部分函数依赖,第三范式解决了非主属性对码的传递依赖问题;那么有没有想过这样问题,假如当前有个表里面有特别多的主属性,其中主属性里面存在着主属性对主属性有着部分函数依赖和传递函数依赖,这样就会造成表的数据冗余和增删改的异常,其实这种主属性对主属性的函数依赖还是少见的,但是真的遇到了可以使用BCNF进行个表拆分。

如一张表U = {p1,p2,np1,np2}
    其中主属性有(p1,p2)非主属性(np1,np2);
    这时需要满足第二范式则np1字段和np2字段必须完全依赖(p1,p2),缺一个码都不行
BCNF关系模式的性质:
    ①:所有非主属性都完全函数依赖于每个候选码
        如一张表U={p1,p2,np1,np2};其中主属性有(p1,p2)非主属性(np1,np2)
        那么候选码和主码都为(p1,p2),下面就可以进行推断:
        通过(p1,p2)-> np1 ,通过(p1,p2)-> np2 都是符合上面的定义
        但是(p1)-> np1 这就不符合上面定义的非主属性都完全函数依赖于每个候选码,因为np1只部分函数依赖于p1
    ②:所有主属性都完全函数依赖于每个不包含它的候选码
        比如现在有个表U={p1, p2, p3, np1, np2};其中主属性有(p1,p2,p3)非主属性(np1,np2)
        现在(p1,p2)和(p2,p3)都是候选码,其中选择(p1,p2)为主码
        那么现在(p1,p2)-> p3,也就是说要得出p3信息则必须知道p1和p2的信息,这样才符合上面的定义
        但是(p1)->p3,在这里p3只依赖主属性p1,p3并没有完全依赖于(p1,p3)候选码,则不符合上面的定义
    ③:没有任何属性完全函数依赖于非码的任何一组属性
示例1:现在有如下一张课程表属性为U = {Cno(课程编号), Cname(课程名称), Cdesc(课程说明)} ①:它只有一个码Cno,没有任何属性(主/非主属性)对Cno产生部分函数依赖或传递函数依赖;--满足第三范式 ②:课程表中的Cno是唯一决定因素;--满足BCNF 如Cno可以决定Cname的值,Cno可以决定Cdesc的值;简化就是Cno->Cname,Cno->Cdesc -- 针对示例1分析是不是BCNF 判断是否为BCNF第一种方式,只需要把依赖关系都列出来如: Cno->Cname,Cno->Cdesc;只要箭头最左边是码(候选码,主码),那么就是BCNF 判断是否为BCNF第二种方式,只需要判断主属性内部是否存在部分函数依赖和传递函数依赖,如果不存在则是BCNF; 因为当前只要Cno主属性,则不可能存部分或者传递函数依赖 示例2:现在有如下一张学生表属性为U ={Sno(学号), Sname(姓名), Sdept(专业), Sage(年龄)} 假定Sname也具有唯一性,那么学生表就有两个码,分别为Sno(学号), Sname(姓名);这两个码都是由单个属性组成,彼此不相交。 ①:首先可以判断非主属性不存在对码的传递和部分依赖,则肯定满足第三范式; ②:学生表中除Sno,Sname外没有其它决定因素,所以学生表为BCNF; -- 针对示例2分析是不是BCNF 判断是否为BCNF第一种方式,只需要把依赖关系都列出来如: Sno->Sname,Sno->Sdept,Sno->Sdept,Sname->Sno,Sname->Sdept,Sname->Sdept; 只要箭头最左边是码(候选码,主码),那么就是BCNF 判断是否为BCNF第二种方式,只需要判断主属性内部是否存在部分函数依赖和传递函数依赖,如果不存在则是BCNF; 因为当前只要Sno,Sname主属性,则不可能存部分或者传递函数依赖 示例3:现在有一张表SJP表属性为U={S(学生),J(课程),P(名次)} 如上表关系:每一个学生选修每门门课程的成绩有一定的名次,每门课程中每一名次只有一个学生(即没有并列名次); 其中分析主属性(S,J,P), ①:由语义可得到函数依赖: (S,J)->P; (J,P)->S ②:(S,J)与(J,P)都可以作为候选码。 ③:关系模式中没有非主属性对码传递依赖或部分依赖,所以满足第三范式 ④:除(S,J)与(J,P)以外没有其它决定因素,所以为BCNF 判断是否为BCNF第一种方式,只需要把依赖关系都列出来如:(S,J)->P; (J,P)->S; 只要箭头最左边是码(候选码,主码),那么就是BCNF 示例4:现在有一张表STJ表属性为U={S(学生),T(教师),J(课程)} 如上表关系:每一教师只教一门课。每门课有若干教师,某一学生选定某门课,就对应一个固定的教师 其中分析主属性(S,J,P),候选码为(S,J), (S,T) ①:由语义可得到函数依赖: (S,J)->T, (S,T)->J, T->J (S,J)->T:通过学生和课程字段可以得出教师 (S,T)->J:通过学生和教师字段可以得出课程 T->J:因为每一教师只教一门课,所以通过教师可以得出课程 ②:关系模式中没有非主属性对码传递依赖或部分依赖,所以满足第三范式 ③:因为T是决定因素,而T不包含码,所以当前表不满足BCNF 判断是否为BCNF第一种方式,只需要把依赖关系都列出来如:(S,J)->T, (S,T)->J, T->J; 只要箭头最左边是码(候选码,主码),那么就是BCNF;但是当前案例中T不是具体的码(候选码),则不是BCNF 针对示例4不是BCNF的关系模式,我们可以通过分解表成为BCNF: 如:示例4的STJ表可以分解为:ST(S,T)与TJ(T,J),分解的这2张表都满足BCNF 拆分速记“谁跟你好,复制自己,把它带走” 总结:3NF和BCNF是在函数依赖的条件下对模式分解所能达到的分离程度的测度。 ①:一个模式中的关系模式如果都属于BCNF,那么在函数依赖范畴内,它已实现了彻底的分离,已消除了插入和删除的异常 ②:3NF的“不彻底”性表现在可能存在主属性对码的部分函数依赖和传递函数依赖

.