第5章_数据库相关
1.CAP定理
- 概述
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值,即写操作之后的读操作,必须返回该值。(分为弱一致性、强一致性和最终一致性)
- 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
- 取舍策略
- CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。传统的关系型数据库RDBMS:Oracle、MySQL就是CA。
- CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
- AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。1
2.事务的ACID
特性
- 原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
- 一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
- 隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
- 持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。4
详见4
3.并发异常
- 脏读
脏读指的是一个事务A在运行时读取了另一个失败事务B未提交的数据,这就导致事务B回滚后,事务A读取到了一个与数据库记录冲突的错误数据。
数据库中某条数据的money值为20,在处理过程中,事务B修改值为100,随后被事务A读取,接下来事务B因为其他环节的错误导致回滚,money的值被还原为20,这时事务A所读取的数据即为脏数据(错误数据)。- 脏写
脏写指的是一个事务A一同回滚了另外一个事务B已经提交的数据,这将导致事务B已经执行成功的操作一同被回滚。
事务B先于事务A执行成功,但随后事务A回滚,将事务B的提交内容一同回滚。- 更新丢失
更新丢失指的是事务A与事务B均提交成功,但是由于读取和写入的时间点问题,导致事务B的修改结果好像未生效一样。
最初数据库中money的值为20,在事务B提交成功后money的值为40,按正常逻辑事务A将money的值减去20,结果应为20,但对事务A来说读取到的值为20,导致最后结果为0。- 不可重复读
不可重复读指的是由于另外一个事务B对数据的操作,导致事务A前后两次读取到的结果不一致。与脏读的主要区别是:一个读取的是后来被回滚的数据,一个读取的是已经成功提交后的数据,但前后并不一致。- 幻读
幻读指的是读取某个范围的数据时,因为有其他事务的操作导致前后两次的查询结果不同。不可重复读与幻读的主要区别在于不可重复读是一条具体数据的不一致,幻读是对一组数据的前后不一致。
引自 5
详见5
4.隔离级别
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。6
5.Oracle
、MySQL
、SQL Server
的事务隔离级别
Oracle MySQL SQL Server 支持
- Read Committed
提交读- Serializable
可串行化
- Read Uncommitted
未提交读- Read Committed
提交读- Repeatable Read
可重复读- Serializable
可串行化
- Read Uncommitted
未提交读- Read Committed
提交读- Repeatable Read
可重复读- Serializable
可串行化- Snapshot
快照- Read Committed Snapshot
已经提交读隔离默认 Read Commit
提交读Repeatable Read
可重复读Read Commit
提交读引自8
详见8
6.范式(Normal Form
)
设计范式是为了解决实际遇到的一些问题:9
1、数据冗余
所谓数据冗余,是指一个表中的一个或多个属性发生改变的时候,会出现多个或更多的属性的数据重复出现。也就是说,属性与属性之间、属性与属性组之间或者属性组与属性组之间存在着一对多或者多对多的数据关系。如下表:
课程C 教员T 参考书B 物 理 李 勇 普通物理学 物 理 李 勇 光学原理 物 理 李 勇 物理习题集 物 理 王 军 普通物理学 物 理 王 军 光学原理 物 理 王 军 物理习题集 数 学 李 勇 数学分析 数 学 李 勇 微分方程 数 学 李 勇 高等代数 数 学 张 平 数学分析 数 学 张 平 微分方程 数 学 张 平 高等代数 2、插入异常
所谓插入异常,是指某个属性,特别是主键,为空,则尽管其他的属性有数据,也无法插入。
假定选课关系表为SelectCourse(学号,姓名,年龄,课程名称,成绩,学分),关键字为组合关键字(学号,课程名称),即(学号,课程名称) → (姓名,年龄,成绩,学分) 。同时其存在一下联系:
(课程名称) → (学分)
(学号) → (姓名,年龄)
对于这个表SelectCourse,假设要开设一门新的课程,暂时还没有人选修。这样,由于还没有"学号"关键字,课程名称和学分也无法记录入数据库。
3、删除异常
所谓删除异常,是指由于某个原因,需要删除表中的某些没用的属性,但是同时也会把其他的一些有用的属性的数据也同时删除掉。
同样是对于上面的表SelectCourse,当学生毕业时,需要从系统里面删除掉毕业学生的所有信息,同时也会把课程的信息也一并删除掉。
4、更新异常
所谓更新异常,是指如果某个属性的数据发生改变的时候,就需要对应属性修改很多的数据。
同样是对于上面的表SelectCourse,若调整了某门课程的学分,数据表中所有行的"学分"值都要更新,否则会出现同一门课程学分不同的情况。
总之,这些属性的数据之间存在的这些问题,一来可能会占用过多的空间,影响查询的效率,二来也会为后面的编程带来不必要的麻烦,三来还会影响数据库的完整性。为此,有必要对这些表格进行范式的优化。
- 第一范式
符合1NF的关系中的每个属性都不可再分。10
举例如下:
公司名称 | 公司地址 | 商品 | 公司电话 | |
名称 | 数量 |
存在二级表头,商品有名称和数量两个属性,需要拆分开来,改造如下:
公司名称 | 公司地址 | 商品名称 | 商品数量 | 公司电话 |
实际上,1NF是所有关系型数据库的最基本要求,你在关系型数据库管理系统(RDBMS),例如SQL Server,Oracle,MySQL中创建数据表的时候,如果数据表的设计不符合这个最基本的要求,那么操作一定是不能成功的。也就是说,只要在RDBMS中已经存在的数据表,一定是符合1NF的。10
- 第二范式
2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖。10
码:可唯一标识实体的属性集。比如学号是学生的码,一个学号唯一标识一名学生。学号和课程号是成绩的码,因为学号和课程号唯一标识一门课程的成绩11
举例如下:
课程名称 | 学号 | 学生姓名 | 学生分数 | 系名 | 系主任 |
高等数学 | 1001 | 李小明 | 95 | 经济系 | 王强 |
大学英语 | 1001 | 李小明 | 87 | 经济系 | 王强 |
普通化学 | 1001 | 李小明 | 76 | 经济系 | 王强 |
高等数学 | 1002 | 张莉莉 | 72 | 经济系 | 王强 |
大学英语 | 1002 | 张莉莉 | 98 | 经济系 | 王强 |
计算机基础 | 1002 | 张莉莉 | 88 | 经济系 | 王强 |
高等数学 | 2003 | 高芳芳 | 82 | 法律系 | 刘玲 |
法学基础 | 2003 | 高芳芳 | 82 | 法律系 | 刘玲 |
在这张表当中,我们可以看到,码
是学号、课程名称。因为可能存在重名的情况,所有学生姓名不是主属性。即通过码
,我们可以确定一条记录,即:其唯一标识一条记录。
但是我们可以看到,对于非主属性系名
、系主任
,其仅依赖于码
当中的学号,而与课程名称无关,即其部分依赖于码
。
这样的后果就是记录的不必要重复,我们可以根据依赖将其拆分为两个表:
学号 | 课程名称 | 分数 |
1001 | 李小明 | 95 |
1001 | 李小明 | 87 |
1001 | 李小明 | 76 |
1002 | 张莉莉 | 72 |
1002 | 张莉莉 | 98 |
1002 | 张莉莉 | 88 |
2003 | 高芳芳 | 82 |
2003 | 高芳芳 | 82 |
以及
学号 | 姓名 | 系名 | 系主任 |
1001 | 李小明 | 经济系 | 王强 |
1002 | 张莉莉 | 经济系 | 王强 |
2003 | 高芳芳 | 法律系 | 刘玲 |
- 第三范式
3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖。10
用上面的图举例:
学号 | 姓名 | 系名 | 系主任 |
1001 | 李小明 | 经济系 | 王强 |
1002 | 张莉莉 | 经济系 | 王强 |
2003 | 高芳芳 | 法律系 | 刘玲 |
我们可以看到系名和系主任是一一对应的,也就是说,我们可以认为系名依赖于系主任,系主任依赖于学号;或者说系主任依赖于系名,系名依赖于学号。总之存在非主属性对于码的传递依赖,产生了不必要的重复。
所以我们可以拆分如下:
学号 | 姓名 | 系名 |
1001 | 李小明 | 经济系 |
1002 | 张莉莉 | 经济系 |
2003 | 高芳芳 | 法律系 |
以及
系名 | 系主任 |
经济系 | 王强 |
法律系 | 刘玲 |
BC
范式
在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。10
举一个新例子:
仓库名 | 管理员 | 物品名 | 数量 |
上海仓 | 张三 | A | 30 |
上海仓 | 张三 | B | 40 |
北京仓 | 李四 | A | 50 |
北京仓 | 李四 | B | 60 |
这张表的主属性有点长,包括:仓库名、管理员、物品名,其可以确定非主属性:数量
在主属性当中,我们可以看到仓库名和管理员是一一对应的,存在着主属性对于码的部分函数依赖与传递函数依赖,造成了重复。
所以我们可以拆分如下:
仓库名 | 物品名 | 数量 |
上海仓 | A | 30 |
上海仓 | B | 40 |
北京仓 | A | 50 |
北京仓 | B | 60 |
以及
仓库名 | 管理员 |
上海仓 | 张三 |
北京仓 | 李四 |
- 第四范式
当一个表中的非主属性互相独立时(3NF),这些非主属性不应该有多值9
举例如下:
学号 | 姓名 | 选课 |
001 | 王小明 | 数学 |
001 | 王小明 | 语文 |
我们可以看到,因为存在重名的情况,所以学号是主属性。而非主属性姓名和课程独立,但是课程存在多值。也就是说存在重复,我们可以分解为:
学号 | 选课 |
001 | 数学 |
001 | 语文 |
以及
学号 | 姓名 |
001 | 王小明 |
- 第五范式
第五范式(5NF):是最终范式。消除了4NF中的连接依赖。9
举例如下:
课程 | 教师 | 参考书 |
英语 | Bill | 人教版英语 |
英语 | Bill | 美版英语 |
英语 | Jay | 人教版英语 |
英语 | Jay | 美版英语 |
英语 | Dave | 人教版英语 |
英语 | Dave | 美版英语 |
高数 | William | 人教版高数 |
高数 | William | 美版高数 |
这个表的大概意思是,假设学校中的某一门课程(英语),由多名教师讲授(Bill,Jay,Dave),并且使用同一套参考书(人教版英语,美版英语)
我们可以看到,这里面全是主键,全码,但存在多值依赖:
一个关系,至少存在三个属性(A、B、C),才能存在这种关系。对于每一个A值,有一组确定的B值和C值,并且这组B的值独立于这组C的值。12
多值依赖的具体解释详见13
总之就是造成了重复,仅看课程和教师,我们发现Bill
的名字出现了2
次;而关注课程和参考书,我们发现人教版英语重复了3
次。
拆分如下:
课程 | 教师 |
英语 | Bill |
英语 | Jay |
英语 | Dave |
高数 | William |
以及
课程 | 参考书 |
英语 | 人教版英语 |
英语 | 美版英语 |
高数 | 人教版高数 |
高数 | 美版高数 |
关于前三个范式,我们可以总结为:12
第一范式:每个表应该有唯一标识每一行的主键。
第二范式:在复合主键的情况下,非主键部分不应该依赖于部分主键。
第三范式:非主键之间不应该有依赖关系。
7.反范式
严格遵守范式设计出来的数据库,虽然思路很清晰,结构也很合理,但是有时却要在一定程度上打破范式设计。范式越高,设计出来的表可能越多,关系可能越复杂,但是性能却不一定会很好,因为表一多,就增加了关联性。特别是在高可用的OLTP(on-line transaction processing)数据库中,这一点表现得很明显,所以就引入了反范式。
不满足范式的模型就是反范式模型。反范式跟范式所要求的正好相反,在反范式的设计模式中,可以允许适当的数据冗余,用这个冗余可以缩短查询获取数据的时间。反范式其本质上就是用空间来换取时间,把数据冗余在多个表中,当查询时就可以减少或者避免表之间的关联。反范式技术也可以称为反规范化技术。
反范式的优点:减少了数据库查询时表之间的连接次数,可以更好地利用索引进行筛选和排序,从而减少了I/O 数据量,提高了查询效率。
反范式的缺点:数据存在重复和冗余,存在部分空间浪费。另外,为了保持数据的一致性,必须维护这部分冗余数据,因此增加了维护的复杂性。所以,在进行范式设计时,要在数据一致性与查询之间找到平衡点,因为符合业务场景的设计才是好的设计。
在RDBMS 模型设计过程中,常常使用范式来约束模型,但在NoSQL 模型中则大量采用反范式。常见的数据库反范式技术如下:
增加冗余列:在多个表中保留相同的列,以减少表连接的次数。冗余法以空间换取时间,把数据冗余在多个表中,当查询时可以减少或者避免表之间的关联。
增加派生列:表中增加可以由本表或其他表中数据计算生成的列,减少查询时的连接操作并避免计算或使用集合函数。
表水平分割:根据一列或多列的值将数据放到多个独立的表中,主要用于表的规模很大、表中数据相对独立或数据需要存放到多个介质的情况。
表垂直分割:对表按列进行分割,将主键和一部分列放到一个表中,主键与其他列放到另一个表中,在查询时减少I/O 次数。16
关于反范式的其他解释详见17
8.索引的使用原则
- 在大表上建立索引才有意义
- 在WHERE子句或是连接条件经常引用的列上建立索引
- 索引的层次不要超过4层
- 如果某属性常作为最大值和最小值等聚集函数的参数,那么考虑为该属性建立索引。
- 表的主键、外键必须有索引
- 创建了主键和唯一约束后会自动创建唯一索引
- 经常与其他表进行连接的表,在连接字段上应该建立索引
- 经常出现在WHERE子句中的字段,特别是大表的字段,应该建立索引。
- 要索引的列经常被查询,并只返回表中的行的总数的一小部分
- 对于那些查询中很少涉及的列、重复值比较多的列尽量不要建立索引
- 经常出现在关键字ORDER BY、GROUP BY、DISTINCT后面的字段,最好建立索引
- 索引应该建在选择性高的字段上
- 索引应该建在小字段上,对于大的文本字段甚至超长字段,不适合建索引,对于定义为CLOB、TEXT、IMAGE和BIT的数据类型的列不适合建立索引
- 复合索引的建立需要进行仔细分析。正确选择复合索引当中的前导列字段,一般是选择性较好的字段。
- 如果单字段查询很少甚至没有,那么可以建立复合索引;否则考虑单字段索引。
- 如果复合索引中包含的字段经常单独出现在WHERE子句中,那么分解为多个单字段索引
- 如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段
- 如果既有单字段索引,又有这几个字段上的复合索引,那么一般可以删除复合索引。
- 频繁进行DML操作的表,不要建立太多的索引
- 删除无用的索引,避免对执行计划造成负面影响
9. 存储过程
存储过程是用户定义的一系列SQL语句的集合,涉及特定表或其他对象的任务,用户可以调用存储过程,而函数通常是数据库已经定义的方法,它接收参数并返回某种类型的值,并且不涉及特定用户表。
存储过程用于执行特定的操作,可以接收输入参数、输出参数,返回单个或多个结果集。在创建存储过程时,既可以指定输入参数(IN),也可以指定输出参数(OUT),通过在存储过程中输入参数,可以将数据传递到执行部分;通过使用输出参数,可以将执行结果传递要应用环境。存储过程可以使对数据库的管理、显示数据库及其用户信息的工作更加容易。
存储过程存储在数据库内,可由应用程序调用执行。存储过程允许用户声明变量并且可包含程序流、逻辑以及对数据库的查询。
具体而言,存储过程的优点如下所示:
- 存储过程增强了SQL语言的功能和灵活性。存储过程可以用流控制语句编写,有很强的灵活性,可以完成复杂的判断和运算。
- 存储过程可保证数据的安全性。通过存储过程可以使没有权限的用户在权限控制之下间接地存取数据库中的数据,从而保证数据的安全。
- 通过存储过程可以使相关的动作在一起发生,从而维护数据库的完整性。
- 在运行存储过程前,数据库已对其进行了语法和句法分析,并给出了优化执行方案。这种已经编译好的过程可极大地改善SQL语句的性能。由于执行SQL语句的大部分工作已经完成,所以,存储过程能以极快的速度执行。
- 可以降低网络的通信量,因为不需要通过网络来传送很多SQL语句到数据库服务器。
- 把体现企业规则的运算程序放入数据库服务器中,以便集中控制。当企业规则发生变化时,在数据库中改变存储过程即可,无须修改任何应用程序。企业规则的特点是要经常变化,如果把体现企业规则的运算程序放入应用程序中,那么当企业规则发生变化时,就需要修改应用程序,工作量非常之大(修改、发行和安装应用程序)。如果把体现企业规则的运算时,只要修改存储过程就可以了,应用程序无须任何变化。
10. 存储过程和函数的区别
存储过程和函数都是存储在数据库中的程序,可由用户直接或间接调用,它们都可以有输出参数,都是由一系列的SQL 语句组成。
具体而言,存储过程和函数的不同点如下所示:
- 标识符不同。函数的标识符为FUNCTION,存储过程为PROCEDURE。
- 函数必须有返回值,且只能返回一个值,而存储过程可以有多个返回值。
- 存储过程无返回值类型,不能将结果直接赋值给变量;函数有返回值类型,在调用函数时,除了用在SELECT 语句中,在其他情况下必须将函数的返回值赋给一个变量。
- 函数可以在SELECT语句中直接使用, 而存储过程不能, 例如:假设己有函数FUN_GETAVG()返回NUMBER 类型绝对值。那么, SQL 语句"SELECT FUN_GETAVG(COL_A) FROM TABLE" 是合法的。
存储过程和函数都可以有输出参数, 都是由一系列的SQL语句组成。