第一章
文件系统处理存储、组织信息弊端
-
数据的冗余和不一致
-
数据访问困难
对于每个新任务需要编写新的应用程序处理
-
数据独立
多个文件具有不同的形式(比如:txt,doc,bin格式等等)
-
完整性问题
- 完整性约束“固化”在程序代码中
- 很难通过修改程序来体现新的约束
-
原子性问题
故障会导致部分更新、使数据库处于不一致状态
-
并发访问异常
- 提高性能
- 可能带来不一致
-
安全性问题
并非数据库系统的所有用户都可以访问所有数据
实例和模式
-
模式-数据库的总体设计
- 物理模式:在物理层描述数据库的设计
- 逻辑模式:在逻辑层描述数据库的设计
- 子模式:描述数据库的不同视图
- 物理数据独立性:应用程序不依赖于物理模式
-
实例-特定时刻存储在数据库中的信息的集合
数据模型
是多种概念工具的集合,用于描述数据库的:
- 数据
- 数据联系
- 数据语义
- 一致性约束
-
关系模型
表,用于表示数据和数据之间的联系
-
实体-联系(E-R)模型
-
实体:区别于其他对象的一件"事情"或一个"物体",由属性集合描述
-
联系:几个实体之间的关联关系
数据库设计:将数据库需求建模为实体和联系的集合
-
-
基于对象的数据模型
E-R模型增加了封装、对象等
-
半结构化数据模型
用XML来表示半结构化数据
数据抽象
-
物理层(Physical level)
描述数据实际上是怎样存储的
-
逻辑层(Logical level)
描述存储在数据库中的数据,以及数据之间的关系,降低耦合(数据库管理员)
为上层应用屏蔽了复杂的底层存储细节,即物理数据独立性
-
视图层(View level)
视图层可以隐藏信息,使用户仅访问数据库的一部分,提高安全性(数据库普通用户)
数据库设计
-
概念设计
设计者选择数据模型,采用所选的数据模型的概念将需求转换为数据库的概念模式,构建实体-联系图,概念模式定义了数据库中表示的实体,实体的属性,实体之间的联系,以及实体与联系上的约束。
概念设计确保所有数据需求都满足,且互相不冲突。通过概念设计还可以去除冗余的特征。
-
逻辑设计-确定数据库模式,找到一个好的模式
如何将概念设计阶段的数据和联系映射到数据库系统的实现数据模型中
设计表结构、主外键、范式
-
物理设计-确定数据库的物理布局
事务管理
事务是数据库应用中完成单一逻辑功能的操作集合
ACID
- **原子性(Atomicity)😗*事务要么发生,要么不发生
- 事务操作完成之后,须保证数据一致性(Consistency)
- 隔离性(Isolation):事务运行期间需独占数据(锁机制)
- 即使有系统故障,数据也要保证持久性(Durability)
- 并发控制管理器控制并发事务间的交互,保证数据库的一致性
- 恢复管理器负责检测系统故障,事务回滚,故障恢复
第二章
关系模式
- 可使用相同属性将不同元组联系起来
关系模式:描述关系中数据逻辑关系的元数据,是一个包含属性名等关系信息的表
关系实例:关系模式在内存运行某个时刻的快照,包含了数据库在运行中实际存储的数据
- 关系模式
- 表的名称
- 列的名称和数据类型
- 主键
- 外键
- 数据库模式
- 所有数据表的关系和结构
- 表之间的关系,如外键关系
- 数据表的索引
- 触发器和存储过程的定义
Key(键,码)
- superkey超码:K能唯一标识关系r®中的一个元组,是一个或多个属性的集合
- K的任意超集也是超码
- 超码可能包含无关紧要的属性
- candicate key候选码:任何真子集都不能成为超码的最小超码
- primary key主码
- foreign key外码:
- 一个关系模式r1在其属性上包含另一个关系模式r2的主码,此属性在r1上称为引用r2的外码,r1称为外码依赖的引用关系,r2称为外码依赖的被引用关系
- 引用完整性约束:参照关系中的任意元组在特定属性上的取值,必须等于被引用关系中的某个元组在该特定关系上的取值
模式图
- 每一个关系模式用一个矩形来表示
- 主码属性用下划线表示
- 外码约束用从参照物关系的外码到被引用关系的主码之间的箭头表示
- 不应与E-R图混淆
关系查询语言
- 命令式/过程化查询语言
- 函数式查询语言
- 声明式/非过程化语言
关系代数
6个基本关系运算
选择运算
选择满足给定谓词条件的元组, σ p ( r ) \sigma_p(r) σp(r)
投影运算
笛卡尔积运算
连接运算
集合并运算
集合差运算
更名运算
第三章
DDL
-
完整性约束(主外码)
SQL禁止破坏完整性约束的数据库更新
- 新插入的元组主码属性为空值、或取值与关系中另一元组的主码属性相同
- 新插入的元组的外码属性未出现在被引用的关系的主码属性中,破坏外码约束
-
关系的安全性和权限信息
create table instructor(
ID char(5) primary key, -- primary key声明的属性会自动为not null
name varchar(20) not null,
dept_name varchar(20),
salary numeric(8,2),
foreign key (dept_name) references department
);
drop table student; -- 删除表和其中内容
delete from student; -- 删除表里的内容,但是保留表(关系模式)
alter table r add A D; -- A是要被添加到关系r的属性的名称,并且D是A的值域,关系中所有元组使用null作为新的属性值
alter table r drop A; -- A是关系r的属性的名称
sql查询的基本结构
-
selete子句
-
列出查询语句需要的属性
-
对应关系代数中的投影操作(属性选择)
-
note:sql语句不区分大小写
-
在查询结果和关系中默认允许重复
-
可以在select后加上关键字distinct强制消除重复
-
*表示所有属性
-
可包含算术表达式
-
-
where子句
- 表示结果必须满足的限定条件
- 对应关系代数的选择操作
-
form 分句
- 列出了查询中用到的关系
- 对应关系代数中笛卡尔积操作
- 如果多关系中存在相同属性,须做区分
-- 对于所有上课的教师,查询他们的姓名及课程ID
select name,course_id
from instructor,teaches
where instructor.ID=teaches.ID;
-- 查询Comp.Sci开的每一门课course ID,semester,year,title
select section.course_id,semester,year,title
from section,course
where section.course_id=course.course_id and dept_name='Comp.Sci';
-
步骤
- 为from子句中列出的关系产生笛卡尔积
- 在1的结果上应用where子句中指定的谓词
- 对于2结果中的每个元组,输出select中指定的属性
note:上述过程不是sql查询语句的执行顺序,sql的查询语言的执行是经过查询优化
-
自然连接:匹配两个关系中所有共同属性的相同值的元组,去掉重复属性列
select name,course_id from instructor,teaches where instructor.ID=teaches.ID; -- 等价于 select name,course_id from instructor natural join teaches;
危险的自然连接:谨防无关的属性具有相同的名字
附加的基本运算
更名运算
old-name as new-name#更改属性名
instructor as T
instructor T
select distinct T.name
from instructor as T, instructor as S -- 笛卡尔乘积
where T.salary > S.salary and S.dept_name =‘Comp. Sci.’
T.name | T.salary | S.name | S.salary |
---|
字符串运算
like
运算符用于模式匹配
- 百分号
%
匹配任意字符串 - 下划线
_
匹配任意一个字符 - 匹配模式是大小写敏感的
-
当匹配模式中含有特殊字符(‘%’, ‘_’, ‘\’),需使用转义字符(通过escape定义)
like '100\%%' escape '\' -- 匹配"100%"开头的字符串
排列元组的显示次序
- desc:降序,asc:升序
where子句谓词
between...and
闭区间
集合运算
找出在2009年秋季开课,或者在2010年春季开课或两个学期都开课的所有课程id号
(select course_id from section where semester='Fall' and year=2009)
union
(select course_id from section where semester='Spring' and year=2010)
- 集合操作
union
、intersect
、except
,自动消除冗余 - 要保留所有的冗余要使用相应的多集操作版本:
union all
、intersect all
、except all
空值
-
所有涉及到空的算术表达式的结果为null
-
涉及空值的任何比较运算的结果的结果返回unknown
-
如果元组在所有属性上取值相等,那么它们就被当做是相同元组,即使某些值为空,
{(‘A’,null),(‘A’,null)}
在去除重复元组时,只保留上述元组的一个拷贝
但是,(‘A’,null)=(‘A’,null)的逻辑判断结果为unknown
distinct子句和谓词中对待空值的方式不同
重复和相等是不同的
-
关于unknown
如果p等于unknown,则"P is unknown"为真(当作false处理,不计入结果集)
聚集函数
以值得一个集合(集或多重集)为输入,返回单个值得函数
- sum和avg的输入必须是数字集
- min,max返回一个标量
分组聚集
group by
- 在select子句中出现,但没有出现在group by子句中的属性,只能出现在聚集函数的内部
having
-
分组限定条件
-
having 子句中的谓词在形成分组之后才起作用,因此可以使用聚集函数
空值和聚集
- sum求和运算忽略输入集合中工资为Null的值
- 除了
count(*)
之外,所有聚集函数都忽略输入集合中的空值 - 如果聚集函数输入集合只有空值
- count函数运算返回0
- 其他聚集函数都返回null
- 聚集函数一般在
select
,having
函数中使用(分组之后)
分组聚集规定
having子句中的谓词在形成分组之后才起作用,因此可以使用聚集函数
MariaDB [university]> select dept_name,avg(salary)
-> from instructor
-> group by dept_name
-> having avg(salary)>42000 and dept_name like 'com%';
MariaDB [university]> select dept_name,avg(salary)
-> from instructor
-> where dept_name like 'com%'
-> group by dept_name
-> having avg(salary)>42000;
sql语句操作顺序
- 根据from子句计算出一个关系
- 应用where子句中的谓词
- 满足where谓词的元组通过group by子句形成分组
- having 子句若存在,就将其作用于每一分组,不符合having子句谓词的分组将被抛弃
- 剩余的分组被select子句用来应用聚集函数产生查询结果元组
嵌套子查询
-
子查询是嵌套在另一个查询中的select-from-where表达式
-
通常用于对集合的成员资格(是否在集合中)、集合的比较以及集合的基数进行检查
-
集合成员资格测试
-
空关系测试
-
from子句的子查询
-
标量子查询
- 可以出现在select、where、having中
- 如果子查询被执行后其结果中不止有一个元组,则产生一个运行错误
-
不带from子句的标量
-
第四章
连接表达式
-- 两个查询等价,查询结果无重复属性
select *
from course
natural join prereq;
select *
from course
join prereq
using course_id;
-- 查询结果保留重复属性,完全等价
select *
from course
join prereq
on course.course_id=prereq.course_id;
select *
from course,prereq
where course.course_id=prereq.course_id;
使用on条件两个优点:
- 在on子句中指定连接条件,在where子句中明确其他限定条件,使得sql语句更加简洁易懂
- on条件子句在外连接的表现和where子句不同(where子句不保留NULL值)
on连接,属性名称可以不一样
natural/using 属性名称要一样
外连接
先执行连接操作,然后将两个关系中不匹配的元组都加到最后的结果关系中,并使用null作为属性值补全,从而保留了在连接中丢失的元组
内连接:不保留未匹配的元组(连接操作join默认为内连接)
自然连接:不保留重复属性
连接关系
-
连接类型:决定了如何处理连接条件中属性不匹配的元组
-
连接条件:决定两个关系中哪些属性相匹配,以及连接结果中是否出现重复属性
对于外连接,where不保留NULL结果,ON保留NULL结果
视图
虚关系:不存储关系,只存储定义
视图定义会导致一个表达式被存储,当使用这个视图时,查询过程中这个表达式会被代入使用
如果定义视图的查询语句对于下列条件都能满足,则称sql视图是可(插入)更新的
- from子句只有一个关系
- select子句只包含关系的属性名,不包含任何的表达式、聚集函数或distinct声明
- 任何没有出现在select子句中的属性内容可以取空值
- 查询中不包含group by或者having子句
- with check option:检查视图更新sql语句中是否满足视图定义中的where子句,不允许不满足视图定义的数据(插入)更新
- 修改更新比插入更新的约束条件弱
物化视图:创建一个关系(物理表),表中包含视图定义的查询结果中的所有元祖
需要定期更新维护
事务
事务具有ACID特性:原子性、一致性、隔离性和持久性
在大多数数据库中:每个sql语句默认为一个事务
原子性保证事务的所有操作在数据库中要么全部反映,要么根本不反映,故障不能让数据库处于事务部分执行后的状态
一致性保证,若数据库开始是一致的,则事务(本身)执行后数据库仍处于一致状态
隔离性保证并发执行的事务相互隔离,使得每个事务感觉不到系统中其他事务的并发执行
持久性保证一旦一个事务提交后,它对数据库的改变不会丢失,即使系统出现故障
完整性约束
主码约束/实体完整性约束:保证关系中的每个元组都是可识别的和唯一的
外码约束/参照完整性约束/引用约束:多个实体和或关系之间的关联关系,用于描述实体之间的联系
用户自定义完整性约束/域完整性/语义完整性约束:关系中属性的取值范围,避免属性的值与应用语义的矛盾
SQL的数据类型与模式
类型转换
-
cast(e as t)
:将表达式e
转换为t
-
使用
coalesce()
函数解决输出空值的情况:接收任意数量的参数(所有参数必须是相同类型),并返回第一个非空参数
默认值
索引
大对象类型
用户自定义类型
域
对CREATE TABLE的扩展
授权
当用户提交查询或更新时,SQL执行先基于该用户曾获得过的权限检查此查询或更新是否是授权
过的。如果査询或更新没有经过授权,那么将被拒绝执行。
-
对数据库用户在某些数据上的权限形式
- select-读取关系,或使用视图完成查询
- insert-插入元祖,可指定属性列
- update-更新,可指定属性列
- delete-删除
- all-允许所有权限
-
修改数据库模式的权限类型
- Index-创建和删除索引
- Resource-创建新的关系
- Alteration-添加或删除关系中的属性
- Drop-删除关系
-
sql为数据库模式指定了一种基本的授权模式:只有模式的拥有者才能够执行对模式的任何修改
-
sql提供了references权限
授权语句grant
grant <权限列表>
on <关系名或视图名>
to <用户或角色列表>
[with grant option];
<用户和角色列表>
- 用户ID
- pubilc,当前和将来所有有效用户
- 角色
- 对视图的授权并不代表对视图相关的实际关系的授权
- 权限授予人必须已经具有对指定项目的权限
收回权限revoke
revoke <权限列表>
on <关系名或视图名>
from <用户、角色列表>
[restrict|cascade];
- 默认级联收回权限(cascade),restrict可用于避免一些不合适的权限级联收回
-
如果某些权限被不同的授权者授予同一个用户两次,那么在一次权限回收后该用户可能仍保有这个权限
-
一个权限被回收后,基于这一权限的其他权限(如视图)也将被回收
第五章
存储过程和函数
- 存储过程(程序)和函数是事先经过编译并存储在数据库中的一套SQL语句
MySQL存储过程的优缺点
优点:
- 有助于减少应用程序和数据库服务器之间的流量,因为应用程序不必发送多个冗长的SQL语句,而只能发送存储过程的名称和参数
- 可重用的和透明的,减少开发工作量
- 安全的,数据库管理员可以向访问数据库中存储过程的应用程序授予适当的权限,而不向基础数据库表提供任何权限
缺点
-
如果使用大量存储过程的每个连接的内存使用量将会大大增加
-
存储过程的构造使得开发具有复杂业务逻辑的存储过程变得更加困难。
-
难以调试存储过程
-
MySQL不提供调试存储过程的功能
-
-
开发和维护存储过程并不容易,移植也比较困难
存储过程或函数说明
- RETURNS子句只能对FUNCTION做指定,对函数而言这是强制的。它用来指定函数的返回类型,而且函数体必须包含一个RETURN value语句
报表查询(高级聚集查询)
-
GROUP BY ROLLUP(A,B,C)
:首先会对(A,B,C)进行group by,然后对(A,B)进行group by,然后是(A)进行group by,最后对全表进行group byROLLUP
注意点:ORDER BY
不能在rollup中使用,两者为互斥关键字- 如果分组中的列包含NULL值,rollup的结果可能不正确,原因在于rollup进行分组统计时,null具有特殊的意义。因此在进行rollup时可以先将null转换成一个不可能存在的值,或者没有特别含义的值
-
GROUP BY CUBE(A,B,C)
:首先会对(A,B,C)进行group by,然后对(A,B),(A,C),(B,C),(A),(B),©最后对全表进行group by
游标和触发器
游标
- 游标从本质上讲是系统为用户开设的一个数据缓冲区,用于存放sql语句从数据库检索出来的结果集
- 当应用程序需要对结果集进行处理时,使用游标可以从结果集中一条条地提取记录,为此每个游标必须有一个名字
基本步骤
- 声明游标:
declare cursor_name cursor for select_statement
- 打开游标:
open cursor_name
- 使用游标:
fetch cursor_name into [varname]
- 关闭游标:
close cursor_name
说明:
游标必须在声明处理程序之前被声明,并且声明变量和条件之后
select语句中不能有into子句
-
游标允许应用程序对查询语句select返回的行结果集中每一行进行相同或不同的操作,而不是一次对整个结果集进行同一种操作
-
MySQL游标为只读,不可滚动和敏感
- 只读:无法通过游标更新基础表中的数据
- 不可滚动:只能按照select语句确定的顺序获取行。不能以相反的顺序获取行。此外,不能跳过行或跳转到结果集中的特定行
- 敏感:敏感游标指向实际数据,不敏感游标使用数据的临时副本。敏感游标比一个不敏感的游标执行得更快,因为它不需要临时拷贝数据。但是,对其他连接的数据所做的任何更改都将影响由敏感游标使用的数据。因此,如果不更新敏感游标所使用的数据,则更安全。MySQL游标是敏感的
触发器
- 触发器定义了一系列操作,称为触发程序,当触发事件发生时,触发程序会自动运行
- 触发器主要用于监视某个表的insert,update,delete等更新操作,这些操作可以分别激活该表的insert,update和delete类型的触发程序运行,从而实现数据的自动维护
-
创建触发器
create trigger trigger_name trigger_time trigger_event on tbl_name fro each row trigger_stmt
trgger_name
:触发器的名称,触发器在当前数据库中必须具有唯一性名称。如果要在某个特定的数据库中创建,名称前面应该加上数据库的名称trgger_time
:触发器被触发的时间。可以是before或者after,以致命触发器实在激活它的语句之前或之后触发。如果希望验证新数据是否满足使用的限制,可以使用before,如果希望在激活触发器的语句执行之后完成几个或更多的改变,可以使用aftertrigger_event
:指明了激活触发器的语句的类型tbl_name
:与触发器相关联的表名。必须使用永久性表。不能将触发程序与temporary表或视图关联起来。for each row
:指定对于受触发事件影响的每一行都要激活触发器的动作trigger_stmt
:是当触发程序激活时执行的语句。如果打算执行多个语句,可以使用begin…end复合语句结构。这样,就能使用存储子程序中允许的相同语句
before触发器中,可以对insert和update的new值进行修改,在after触发器中,则不能new值
第七章
设计过程
-
设计阶段
首先确定实体集,联系集,及实体集和联系集中的属性
- 概念设计阶段:构建实体-联系图
- 逻辑设计阶段:将实体-联系图映射到关系模式
- 物理设计阶段:指明数据库文件组织格式和索引格式
实体-联系模型(E-R模型)
三要素
实体集、联系集、属性
一个数据库可以被建模为:
- 实体的集合
- 实体间的联系
联系集中实体的角色
- 实体在联系中扮演的功能称为实体的角色
- 当同样的实体集参与一个联系集多于一次,这类联系集称作是递归联系集
- 例如:课程先修关系prereq
联系集的度
-
参与联系集的实体集的数目称为联系集的度
-
递归联系集的度是1
属性
- 简单和复合属性
- 单值和多值属性
- 如phone-number
- 基属性和派生属性
- 派生属性可以从别的属性中派生出来
- 派生属性不存储,而是在需要时计算出来
约束
映射基数
映射基数(基数比率),表示一个实体通过一个联系集
对二元联系集来说,映射基数必然是以下情况之一:
- 一对一
- 一对多
- 多对一
- 多对多
注意:A和B中的某些元素可能并不映射到另一集合中的任一元素
参与约束
-
如果实体集E中的每个实体都参与到联系集R的至少一个联系中,实体集E在联系集R中的参与称为全部的
-
如果实体集E中只有部分实体参与到R的联系中,实体集E在联系集R中的参与称为部分的
实体集的码
- 超码:一个或多个可以用来唯一地表示实体的属性(集)
- 候选码:最小的超码
- 主码:用来区分实体的候选码,主体只有一个,候选码可以有多个
联系集的码
表示一对实体集在一个特定的联系集中只能有一个联系
(一对一或一对多时可以取多方实体集的主码做主码,不用全部属性)
实体-联系图
E-R图
参与联系集中的实体集
- 全部参与(用两条线标识):实体集中的每个实体都参与到联系集的至少一个联系中
- 部分参与:某些实体不参与到联系集中的任何一个联系
具有属性的联系集
- 联系集的属性用虚线连接
基数约束
- 一个箭头代表"一"
- 一条线段代表"多"
(两次一出现的地方就是箭头指向的地方)
一对一
一对多
多对一
多对多
基数限制也可以用参与约束来表示
具有复合、多值、派生属性的实体
- name:复合属性
- phone_number:多值属性
- age:派生属性
角色
- 一个联系的实体集不需要唯一
- 一个实体集中的元素每次出现在关系中代表一个“角色”
弱实体集
-
一个没有足够属性形成主码的实体集叫做弱实体集
但是加入其他属性之后就会出现冗余
-
弱实体集必须与标识实体集关联才有意义
- 弱实体集存在依赖于标识实体集
-
将弱实体集与其标识实体集相连的联系称为标识性联系
- 标识性联系以双边框的菱形表示
-
弱实体集通过一个全部参与的、(多对一或一对一)的联系集与标识实体集联系
-
弱实体集的分辨符区分弱实体集中实体的属性集合,也称为该弱实体集的部分码
- 弱实体集的分辨符用虚下划线表示
-
弱实体集的主码由标识实体集的主码和弱实体集的分辨码共同组成
-
弱实体集可以参与标识性联系以外的其他联系,也可能与不止一个标识实体集关联
-
在某些情况下可以选择将一个弱实体集表示为标识实体集的一个多值(复合)属性
- 如果弱实体集只参与标识性联系,且属性不多,则建模时更适合将其作为属性
- 反之,更适合将其表示为弱实体集
转换为关系模式
具有简单属性的实体集的表示
-
从强实体集转换而来的模式与强实体集具有相同属性、主码
-
从弱实体集转换的模式包含弱实体集的属性和标识强实体集的主码
- 需要建立相应的外码约束和完整性约束(如级联删除)
复合属性
- 复合属性通过为每个子属性创建单独的属性
- 实体集instructor有复合属性name:由first_name**、**middle_initial和last_name构成
- 转化关系模式后,该实体集具有name_first_name**、**name_middle_name和name_last_name
多值属性
-
实体集E的多值属性M用一个独立的模式EM表示
-
模式EM包含对应于E主码及多值属性M的属性
-
多值属性的每个值映射到关系模式EM的每一个独立元祖
- 一个实体instructor的主码22222及电话号码456-7890和123-4567映射到两个元组:(22222, 456-7890) 和 (22222, 123-4567)
- 注意:在关系模式EM上建立参照实体集E关系模式主码的外码约束
time_slot实体除了主码外只有一个属性,同时该属性是多值属性
不需要建立一个对应于实体集的模式,只需创建一个对应于多值属性的关系即可
- time_slot(time_slot_id,day,start_time,end_time)
- sec_time_slot中的time_slot_id属性在这样处理后不能作为被参照的外码,因为其已经不是主码了,被参照的外码必须是某个关系模式中的主码
联系集的表示
注意:需要为联系集的属性建立外码约束
模式的冗余——合并
-
多对一和一对多的联系集的模式,如果“多”方的参与是全部的,那么可以将“多”方实体集和联系集的模式合并成单个包含两个模式所有属性并集的关系模式
合并后模式中加入原联系集的外码约束
-
如果"多"方参与是部分的,也可以通过使用空值来进行模式合并。转换为关系模式时,联系集中"一"方的相关属性不能设为not null
全部参与的话,相关属性应设置为not null
-
在一对一联系的情况下,联系集的关系模式可以跟参与联系的任何一个实体集的模式进行合并
-
一般而言,连接弱实体集与其标识强实体集之间的标识性联系集转换出的模式是冗余的
- section(弱实体)的关系模式已经包含了出现在sec_course模式中的属性
实体-联系设计问题
设计问题
使用实体集 vs 属性
多值复合属性
常见错误
- 用一个实体集的主码作为另一个实体集的属性
- 将相关实体集的主码作为联系集的属性
联系属性的布局
设计时将联系集的描述性属性作为联系集的属性还是实体集的属性这一选择应由映射基数决定
- 一对多联系集的属性仅可以放到参与联系的"多方"实体集中
- 一对一联系集的属性可以放到任意一个参与联系的实体集中
- 当一个属性是由参与的实体集联合确定而不是由单独的某个实体集确定时,该属性就必须放到多对多联系集中。
扩展的E-R特性
特化
-
自顶向下:从初始实体集到一系列不同层次的实体集的细化
-
在实体集内部进行分组的过程称为特化
-
子集成为较低层的实体集,可能具有高层实体集不具有的属性,或者参与到高层实体集不参与的联系集中
-
特化通过特化实体指向另一个实体的空心箭头来表示
不相交
- 一个实体至多属于一个低层实体集
- E-R图中多个低层实体集连接到同一个空心箭头
重叠
- 同一实体可同时属于同一概化的多个低层实体集
概化
- 自底向上:多个实体集根据共同具有的特征综合成一个较高层的实体集
- 概化是高层实体集与一个或多个低层实体集间的包含关系
- 概化是特化的逆过程,它们在E-R图中的表示是相同的
一个实体集可以根据不同的特征来进行多次特化
高层和低层实体集可以形成超类-子类
特化/概化的设计约束
完全性约束
- 全部(total):每个高层实体集必须属于一个低层实体集
- 部分(partial):允许一些高层实体不属于任何低层实体集
默认部分概化
属性继承
低层实体集继承了与其相关联的高层实体集的所有属性,并且继承地参与到其高层实体所参与的联系集中
聚集
将聚集实体集作为单一单元,不必考虑其内部结构
总结
第八章
关系数据库设计
好的设计的特点
有损分解
无法通过自然连接重建原始关系元组的分解
无损分解
函数依赖
-
超码:在某关系中,若一个或多个属性的集合{A1,A2,…,An}函数决定该关系中的其他全部属性,则称该属性为该关系的超码
若属性组K满足 K → R K\rightarrow R K→R,则K是关系模式R的超码
-
候选码:若集合{A1,A2,…,An}的任何真子集均不能函数决定该关系中的其他属性,则此时{A1,A2,…,An}是最小的超码
K → R K\rightarrow R K→R且不存在 α ⊂ K \alpha \subset K α⊂K,满足 α → R \alpha \rightarrow R α→R
-
外码:关系模式R中属性或属性组X是另一关系模式的主码,则称X是R的外码
函数依赖的使用
- 如果关系实例r在函数依赖集F上合法,则称r满足F
- 如果模式R上的所有合法关系实例都满足函数依赖集F,则称F在关系模式R上成立
函数依赖相关术语
- 平凡的函数依赖, β ⊆ α , α → β \beta \subseteq \alpha,\alpha\rightarrow \beta β⊆α,α→β
- 非平凡的函数依赖, α → β , β ⊈ α \alpha \rightarrow \beta,\beta \not\subseteq \alpha α→β,β⊆α
- 若 α → β \alpha\rightarrow \beta α→β,则称 α \alpha α为决定因素
- α → β \alpha\rightarrow \beta α→β称为部分依赖的条件:存在 α \alpha α的真子集 γ \gamma γ,使得 γ → β \gamma\rightarrow \beta γ→β
函数依赖的闭包
闭包 F + F^+ F+:从给定函数依赖集F能够推导出的所有函数依赖的集合
F + F^+ F+是 F F F的超集
函数依赖的逻辑蕴含
-
给定关系模式r®,如果r®的每个满足F的实例也满足某个函数依赖f,则R上的函数依赖f逻辑蕴含于r上的函数依赖集F
-
已知关系R上的函数依赖集T、F,如果对于该关系中满足F的每一个关系实例都满足T,称函数依赖集F逻辑蕴含函数依赖集T
-
若F蕴含于T,且T蕴含于F,则函数依赖集T和F是等价的, T ≡ F T\equiv F T≡F
函数依赖的推理规则
公理是
- 有效的:不会产生错误的函数依赖
- 完备的:对于一个给定的函数依赖集F,能产生整个F+
函数依赖集的闭包
不求函数依赖集的闭包,但是需要求属性依赖集的闭包
属性闭包
给定属性集 α \alpha α,则在函数依赖集F下由 α \alpha α确定的所有属性集合为F下 α \alpha α的闭包,记为 α + \alpha^+ α+
停止条件:
- result=R
- while循环一边后result不再发生变化
候选码:最小的超码
候选码的任何真子集都不能确定R
属性闭包的应用
- 判断超码:若 α + \alpha^+ α+包含R中所有属性,则 α \alpha α是超码
- 验证函数依赖:若 β ⊆ α + \beta \subseteq \alpha^+ β⊆α+,则函数依赖 α → β \alpha\rightarrow \beta α→β成立
- 计算F的闭包:对于任意的 γ ⊆ R \gamma\subseteq R γ⊆R,计算 γ + \gamma^+ γ+,对于任意的 S ⊆ γ + S\subseteq \gamma^+ S⊆γ+,输出函数依赖 γ → S \gamma\rightarrow S γ→S
规范化
规范化的目标
在关系模式不是“好”的模式的情况下,将其分解成关系模式集
- 每个关系模式都是好的模式:无数据冗余(符合一定范式)
- 分解是无损连接分解
- 最好的是,分解是保持依赖的
规范化的方式
- 找候选码,区分主属性和非主属性
- 写出主属性与非主属性的函数依赖关系
- 写出主属性的函数依赖关系
- 写出非主属性的函数依赖关系
- 模式分解
范式
某一关系模式R最高属于第n范式,可简记为 R ∈ n N F R\in nNF R∈nNF
第一范式
-
如果某个域的元素被认为是不可分的单元,那么这个域就是原子的
-
如果一个关系模式R的所有属性域都是原子的,则关系R属于第一范式
-
非原子的值会造成复杂存储和数据冗余
第二范式
若关系模式符合第一范式,且在F+中的每一个非主属性完全函数依赖于候选码,则关系模式符合第二范式。
BCNF范式
对所有F+中形如 α → β \alpha\rightarrow\beta α→β的函数依赖( α ⊆ R \alpha \subseteq R α⊆R且 β ⊆ R \beta \subseteq R β⊆R),下面至少有一个成立:
- α → β \alpha \rightarrow \beta α→β是平凡的函数依赖( β ⊆ α \beta \subseteq \alpha β⊆α)
- α \alpha α是模式R的一个超码
在关系模式R(U,F)中,如果F+中的每一个非平凡函数依赖的决定属性集都包含候选码(即为超码),则r® ∈ \in ∈BCNF。
排除了任何属性(包括主属性和非主属性)对候选码的部分依赖和传递依赖,也排除了主属性之间的传递依赖
判断方式
- 第一种:判断函数依赖的左边是否都包含候选码
- 第二种:判断是否存在主属性对不包含该主属性的候选码的传递依赖或部分依赖
第三范式
对F+中所有形如 α → β \alpha \rightarrow \beta α→β的函数依赖中,至少有以下之一成立。
- α → β \alpha \rightarrow \beta α→β是一个平凡的函数依赖(即 β ⊆ α \beta \subseteq \alpha β⊆α)
- α \alpha α是R的一个超码
- β − α \beta -\alpha β−α的每个属性A都包含在R的候选码中(每个属性也许包含在不同的候选码中)
第三范式允许存在主属性对候选码的传递依赖和部分依赖,在函数依赖集F中用来满足保持某些函数依赖
具有函数依赖集F的关系模式R属于3NF,则R中任何非主属性A既不部分依赖于码也不传递依赖于R的码
消除非主属性对候选码的传递依赖
函数依赖理论
正则覆盖
- F的正则覆盖Fc没有任何冗余依赖或存在冗余部分的依赖
- Fc具有和F相同的函数依赖集闭包
无关属性
- 如果去除函数依赖中的一个属性不改变该函数依赖集的闭包,则称该属性是无关属性
F的正则覆盖Fc是一个函数依赖集,具有以下特性:
-
F逻辑蕴含Fc中的所有函数依赖
-
Fc逻辑蕴含F中的所有函数依赖
-
Fc中任何函数依赖都不含无关属性
-
Fc中函数依赖的左半部都是不同的
函数依赖集的闭包
超码能够函数确定R中的所有属性
判断范式的第一步,找出候选码(确定主属性与非主属性)
无损分解
通过自然连接重建原始关系元组的分解
如果下面的依赖中至少有一个属于F+,那么将R分解为R1和R2是无损分解连接
即 R 1 ∩ R 2 R_1\cap R_2 R1∩R2是R1或者R2的超码
上述测试只是无损连接分解的充分条件
只有当所有约束都是函数依赖时,才是必要条件
保持依赖
限定
判断保持依赖
分解算法
BCNF的判定
定义判定
简化判定
检查关系模式R是否属于BCNF,仅需检查给定集合F中的函数依赖是否违反BCNF,不用检查F+的所有函数依赖
如果F中没有违反BCNF的函数依赖,那么F+中也不会有违反BCNF的函数依赖
但是在检测由关系R分解后的关系Ri是否满足BCNF范式时,不能只使用F
判定分解后是否属于BCNF
分解实例
BCNF和保持依赖
3NF的判定
- BCNF分解可能无法做到保持依赖
解决办法:采用3NF
- 允许冗余(可能会引起问题)
- 但是函数依赖可以在不进行连接操作的情况下在单个关系上检验(BCNF是无损分解,但是可能不能保持依赖)
- 总是能够在无损连接及保持依赖的情况下分解为3NF
第十、十一章
存储管理和索引
文件和记录的组织
- 数据库是以一系列文件的形式存储的。每个文件在逻辑上组织成为记录的一个序列
- 每个文件分为定长的存储单元,称为块(Block)
- 块是存储分配和数据传输的基本单元,块大小一般为4-8KB
定长记录
删除第i个记录的可选方案
-
压缩(后面的记录前移)
-
将记录n移到i
-
不移动记录,在一个空闲列表中将所有空闲记录列出
- 将第一个删除的记录的地址存储于文件头
- 用这第一个记录来存储第二个被删除记录的地址,以此类推(类似指针)
变长记录
- 多种记录类型存储在一个文件中
- 允许一个或多个字段是变长的记录类型
- 允许可重复字段的记录类型
- 属性按照顺序存储
- 以固定大小表示可变长度的属性(偏移量,长度),实际数据存储在所有固定长度的属性后
- 记录末尾加上记录终止符
分槽的页结构
- 块头包括
- 块中记录的条目的个数
- 块中空闲空间的末尾处
- 一个由包含记录位置和大小的记录条目组成的数组
- 记录可以在页中移动,来保持它们连续存储
- 记录相互间没有空闲的空间(若删除记录,需要移动记录数据)
文件中记录的组织
-
堆文件组织:一个记录可以放在文件中的任何地方,只要有空闲空间
-
顺序文件组织:记录根据"搜索码"的值顺序存储
- 删除-使用指针链
- 插入-定位插入的位置(都需更新指针)
- 如果有空闲空间,插入到空闲处
- 如果没有空闲空间,将新纪录插入溢出块
- 需要不时重组文件,使其顺序存放
-
散列文件组织:在每条记录的某些属性上计算一个散列函数。散列函数的结果确定记录应放到文件的哪个块中
多表聚簇文件组织
- 能够很好地处理对关系连接的查询以及涉及一个系和相应教师的查询
- 对只涉及的单个关系的查询效果不好
- 可以添加指针链来连接某个关系的记录
数据字典存储
即系统目录存储元数据(关于数据的数据)
- 关系的信息
- 关系的名字
- 每个关系属性的名字、类型和长度
- 视图的名字和定义
- 完整性约束
- 用户和账户信息,包括密码
- 统计和描述性数据
- 每个关系中的元组数目
- 物理文件组织信息
- 关系如何存储
- 关系的物理位置
- 索引的信息
数据缓冲区
存储访问
-
每个文件分为定长的存储单元,即块。块是存储分配和数据传输的基本单位。
-
目标是减少磁盘和存储器之间传输的块数。减少磁盘访问次数的一种方法是在主存储器中保留尽可能多的块。
-
缓冲区-主存储器中用于存储磁盘块的副本的那一部分
-
缓冲区管理器-负责缓冲区空间分配的子系统。
缓冲区替换策略
- 被钉住的块:不允许写回磁盘的块
- 立即丢弃策略:一旦块中最后一个元组被处理完毕,就立即命令缓冲区管理器释放这个块所占用的空间
- 最近最常使用策略(与LRU相反):系统要替换最近一直在使用的块。当块中最后一个元组处理完毕后,块将被解除钉住
缓冲区管理器可以使用请求访问某个特定关系的统计信息
如数据字典被经常访问,因此将数据字典的块保留在主存储器的缓存中
缓冲区管理器也支持块的强制写出到磁盘
顺序索引
-
搜索码-用于在文件中查找记录的属性或属性集
-
一个索引文件包含如下形式的记录(称为索引项)
-
**顺序索引:**按顺序存储搜索码的值,并将每个搜索码与包含该搜索码的记录关联起来
-
**主索引:**顺序文件组织中,索引的搜索码指定了文件中记录的顺序(一个关系只有一个)
- 也称为聚集索引
- 主索引的搜索码一般是主码,但不是必须的
-
**辅助索引:**搜索码指定的顺序与文件中记录的物理顺序不同的索引(一个关系中可以有多个)
- 也称为非聚集索引
-
**索引顺序文件:**在搜索码商有聚集索引的文件
稠密索引
文件中的每个搜索码值有一个索引记录
稀疏索引
只为搜索码的某些值建立索引记录(在记录按照搜索码顺序排列时适用)
- 寻找有搜索值K的记录:
- 找到最大搜索码值小于或等于K的索引项
- 从该索引项指向的记录开始,沿着文件中的指针查找,直到找到所需记录为止
对比
- 稀疏索引插入和删除时所需的空间及维护开销较小
- 稀疏索引定位一条记录的速度比较慢
折中方案
为每个块建一个索引项(块起始搜索码)的稀疏索引
多级索引
将主索引以顺序文件的形式放于磁盘,并为其建立一个稀疏索引
- 外层索引——主索引的稀疏索引
- 内存索引——主索引文件
对文件进行插入或删除操作后,所有级别的索引都需要更新
辅助索引
-
索引记录指向包含所有指向具有特定搜索键值的实际记录的指针
-
辅助索引必须是稠密的,即不可能存在辅助稀疏索引,因为其中搜索码的顺序与记录实际存储的物理顺序一般不一致,且一个索引可能指向多个记录。
对比
使用主索引进行顺序扫描是很高效的,但是使用辅助索引却花费很大,因为:
-
每次对记录的访问都可能从磁盘获得一个新块
-
访问磁盘的时间远远大于访问内存的时间
B+树索引文件
- 顺序存储的缺点
- 性能随着文件的增长而下降,因为创建了许多溢出块
- 需要定期重组整个文件
- B+树索引文件的优势
- 在面对插入和删除时,使用小的局部更改自动重组
- 不需要重组整个文件来保持查询
散列索引
静态散列
-
桶是能存储一条或多条记录的一个存储单元(一个桶就是一个磁盘块)
-
在散列文件组织中,通过使用散列函数直接从搜索码中获得包含该记录的桶
-
散列函数h是一个从K到B的函数。K表示所有搜索码值的集合,B表示所有桶地址的集合
-
具有不同搜索码值的记录可能映射到同一个桶,因此整个桶都要被顺序搜索来定位记录
- 散列索引无法支持范围查询
桶溢出处理
- **溢出链:**一个给定桶的所有溢出桶用一个链接列表链接在一起(闭散列/闭地址)
- **开散列:**桶集合是固定的,没有溢出链,当一个桶满了之后,系统将记录插入到初始桶集合的其他桶中
第十三、十四章
事务管理和并发控制
- 事务是构成单一逻辑工作单元的操作集合
- 事务是访问并可能更新各种数据项的一个程序执行单元
- 事务由事务开始与事务结束之间执行的全体操作组成
- 事务管理主要处理的两个主要问题
- 各类故障恢复,如硬件故障,系统崩溃
- 多个事务的并发执行
原子性要求
事务的所有操作在数据库中要么全部反映出来,要么完全不反映
- 如果在3执行之后,但是6执行之前,该事务出现故障,钱就会“丢失”导致数据库的不一致性
- 故障可能是由硬件或软件导致的
- 系统应该保证部分执行的事务的更新不会体现在数据库中
一致性要求
事务隔离执行时(即在没有其他事务并发执行的情况下)保持数据库的一致性
- 事务执行结束后A和B的总和不变
- 显性的完整性约束:如,主码和外码
- 隐性的完整性约束:如,所有账户的余额总和必须等于转账钱的余额总和
- 事务执行期间,数据库会暂时不一致
- 事务成功执行后,数据库必须保持一致
隔离性要求
尽管多个事务可能并发执行,但是每个事务都感觉不到系统重有其他事务在并发地执行
对于任何一对事务Ti和Tj,在Ti看来,Tj或者在Ti开始之前已经完成执行,或者在Ti完成之后开始执行
- 通过事务串行化来保证隔离性
- 然而,并发执行事务有很大性能上的优势
持久性要求
- 一个事务成功完成后,它对数据库的改变必须是永久的,即使出现系统故障。
事务ACID特性
事务原子性和持久性
事务状态
-
活动的:初始状态,事务执行时处于这个状态
-
部分提交的:最后一条语句执行前
-
失败的:发现正常的执行不能继续后
-
中止的:事务回滚并且数据库已恢复到事务开始执行前的状态后
中止后的选择:
- 重新开始事务
- 只有没有任何内部逻辑错误的时候才能执行
- 杀死事务
- 重新开始事务
-
已提交的:成功完成执行的事务
- 撤销已提交事务所造成影响的唯一方法是执行一个补偿事务
事务隔离性
事务处理系统通常允许多个事务并发执行:
-
提高吞吐量和资源利用率
如一个事务使用CPU,同时另一个事务在读或写磁盘
-
减少等待时间:短事务不必等在长事务后面
减少平均响应时间
并发控制机制——实现事务隔离性的机制
控制并发执行的事务之间的相互作用,来避免他们破坏数据库的一致性。
调度——指令执行顺序,指定并发执行事务的指令执行的时间顺序
-
一组事务的一个调度必须包含这一组事务的全部指令
-
必须保证指令在各个事务中出现的顺序
-
一个事务成功执行后,会有一条指令作为最后的声明
- 事务默认提交指令commit为其最后一条指令
- 一个事务没有成功完成时,会用一条中止指令abort来作为最后的声明
SQL中的事务定义
- sql标准规定事务的开始是隐式的
- 事务的结束用下列sql语句之一来表示
- commit:提交当前事务并开始一个新的事物
- rollback:回滚当前事务
- 在数据库系统中,缺省每个sql语句如果成功执行的话,也立即隐式提交事务
- 这种隐式提交也可以通过数据库指令关闭
可串行化
- 基本假设:每个事务都能保持数据库的一致性
- 事务的串行执行是可以保持一致性的
- 如果一个调度等价于一个串行调度,那么这个调度就是可串行化的
1. 冲突可串行化
2. 视图可串行化
事务的简化视图
- 只考虑read和write
- 假设事务在read和write指令之间可以对本地缓冲区的数据进行任意的操作
- 简化的调度只包含read和write指令
可恢复性
可恢复调度
如果Tj读取了Ti所写的数据项,则Ti须先于Tj提交
T9无法恢复,如果T9在read之后直接提交
如果T8要中止,T9会读到一个不一致的数据A。因此,数据库必须保证调度可恢复。
级联回滚
因单个事务故障导致一系列事务回滚
如果没有一个事务是提交的。如果T10失败,T11和T12必须回滚
会导致撤销大量工作
无级联调度
对于每对事务Ti和Tj,如果Tj读取了先前由Ti所写的数据项,则Ti必须在Tj这一读取操作之前提交。
并发控制
数据库必须提供一种机制来保证所有调度是
- 冲突可串行化,并且
- 可恢复性,最好是无级联调度
并发级别
- 可串行化
- 可重复读:只允许读取已提交数据,一个事务对相同数据的重复读取要返回相同的值(其他事务不得更改该数据)
- 已提交读:只允许读取已提交数据,但不要求可重复读
- 未提交读:允许读取未提交数据
以上所有隔离性级别都不允许脏写:即如果一个数据项已经被另外一个尚未提交或中止的事务写入,则不允许对该数据项执行写操作。
基于锁的协议
- 排他锁X:对数据项既可读又可写。lock-X
- 共享锁S:对数据项只能读。lock-S
申请锁请求发送给并发控制管理器。只有在并发控制管理器授予所需锁后,事务才能继续其操作。
- 如果被请求锁与数据项上已有的锁相容,那么事务可以被授予该锁
- 一个数据项可以同时有多个共享锁
- 如果一个事务在某个数据项上拥有排他锁,那么其他事务不能再在这个数据项上加任何锁。
- 如果一个锁不能被授予,那么请求该锁的事务必须等待,直到该数据项上的其他不相容锁全部释放,然后再被授予锁。
上面的锁无法保证可串行化——如果A在read(B)的时候被其他事务更新了,那么最后的和将会是错误的答案。
T3和T4都无法被处理:排他锁lock-X(B)导致T4等待T3释放其在B上的锁,而排他锁lock-X(A)导致T3等待T4释放其在A上的锁
即为死锁,要处理T3或T4其中一个死锁,必须回滚并释放锁。
隐患
-
死锁
-
饥饿
锁的授予
避免事务饿死的授权加锁方式:当事务Ti申请对数据项Q加M型锁时,并发控制管理器授权加锁的条件需满足:
- 不存在在数据项Q上持有与M型锁冲突的锁的其他事务
- 不存在等待对数据项Q加锁且先于Ti申请加锁的事务
封锁协议
- 如果调度S是那些遵从封锁协议规则的事务集的可能调度之一,则称调度S在给定的封锁协议下是合法的
- 一个封锁协议当且仅当其所有合法的调度为冲突可串行化时,称它保证冲突可串行化
- 对于任何合法的调度,其关联的事务的优先关系是无循环的。
两阶段封锁协议
阶段1:增长阶段
- 事务可以获得锁
- 事务不能释放锁
阶段2:缩减阶段
- 事务可以释放锁
- 事务不能获得新锁
封锁点:在调度中该事务获得其最后加锁的位置
- 两阶段封锁协议保证可串行化
- 事务按照封锁点来排序
- 两阶段封锁不能保证不发生死锁
- 两阶段封锁下很有可能发生级联回滚
严格两阶段封锁协议
未提交事务所写的任何数据在该事务提交之前均以排他方式加锁,防止其他事务读取这些数据。
符合严格两阶段封锁协议的一定是无级联调度
强两阶段封锁协议
事务提交之前不得释放任何锁
即并行的两个事务不能存在任何相同的公共数据项
在这个协议下,事务可以按其提交的顺序串行化
多粒度
如果一个事务显式地对树中的某个节点加了锁,那么它也给所有同一模式下的该节点的子节点隐式地加了锁
- 细粒度(树的低层):高并发性,锁开销多
- 粗粒度(树的高层):低并发性,锁开销少
意向锁类型
- 共享型意向锁(IS):将在树的较低层进行显式封锁,但只能加共享锁
- 排他型意向锁(IX):将在树的较低层进行显式封锁,可以加排他锁或共享锁
- 共享排他型意向锁(SIX):以该节点为根的子树显式加了共享锁,并且将在树的更低层显式地加排他锁
意向锁允许较高层的节点被加上共享锁或排他锁,而无须从树根遍历到子孙节点来检验锁的相容性,提升锁相容检验的效率
多粒度封锁模式
事务Ti按如下规则对数据项Q加锁
- 必须遵从锁类型相容函数
- 必须首先封锁树的根结点,并且可以加任意类型的锁
- 仅当事务Ti当前对Q的父节点具有IX或IS锁时,对结点Q可以加S或IS锁
- 仅当事务Ti当前对Q的父节点具有IX时,对结点Q可加X,SIX,IX锁
- 仅当Ti未曾对任何结点解锁时,Ti可对结点加锁(满足两阶段封锁)
- 仅当Ti当前不持有Q的子结点的锁时,Ti可对结点Q解锁
加锁按自顶向下的顺序,锁的释放按自底向上的顺序
基于时间戳的协议
每个数据项Q需要与两个时间戳值相关联
- W-timestamp(Q)表示成功执行write(Q)的所有事务的最大时间戳
- R-timestamp(Q)表示成功执行read(Q)的所有事务的最大时间戳
-
时间戳排序协议保证冲突可串行化
冲突操作按时间戳的顺序来处理
-
保证无死锁
- 不存在等待
- 可能有长事务饿死
-
可能产生不可恢复的调度
基于有效性检查的协议
有效性检查协议要求每个事务Ti在其生命周期中按两个或三个阶段执行
- 读阶段:事务Ti的所有wirte操作都是对局部临时变量进行的
- 有效性检查阶段:事务Ti进行有效性测试,判断是否可以执行write操作而不违反可串行性
- 写阶段:如果Ti已通过有效性检查,则保存任何写操作结果的临时局部变量值被复制到数据库中。只读事务不进入此阶段。
利用时间戳Validation(Ti)的值,通过时间戳排序技术决定可串行化顺序,以增加并发性
TS(Ti)=Validation(Ti)
对于任何满足TS(Tk)<TS(Ti)的事务Tk必须满足下面两条件之一:
即有效性测试通过,Ti可以进入写阶段并提交;
否则测试失败,Ti中止
- 有效性检查协议能够自动预防级联回滚,保证无死锁
恢复系统
- 事务故障
- 系统崩溃
- 磁盘故障
恢复机制
恢复算法
- 在正常事务处理时采取措施,保证有足够的信息可用于故障恢复
- 在故障发生后采取措施,将数据库内容恢复到某个保证数据库一致性、事务原子性及持久性的状态
存储器类型
- 易失性存储器
- 非易失性存储器
- 稳定存储器
数据访问
物理块:位于磁盘上的块
缓冲区:临时位于主存的块
磁盘和主存间的块移动是由下面两个操作引起的
- input(B)传送物理块B至主存
- output(B)传送缓冲块B至磁盘,并替换磁盘上相应的物理块
read/write:缓存到内存
input/output:内存到磁盘
-
每个事务Ti有一个私有工作区,用于保存Ti所访问及更新的所有数据项的拷贝
Ti的工作区中保存的每一个数据项X记为xi
-
read(X)将数据项X的值赋予局部变量xi
-
write(X)将局部变量xi的值赋予缓冲块中的数据项X
output(Bx)不需要立刻在write(X)后执行。系统会在它认为合适的时候执行output操作
-
事务
- 必须在第一次访问X之前执行read(X)
- write(X)可以在事务被提交前的任意时刻执行
恢复与原子性
为保证原子性,必须在修改数据库本身之前,首先向稳定存储器输出信息,描述要做的修改。
目的:确保由中止事务所做的修改不会持久保存于数据库中,即回滚该中止事务
基于日志的恢复机制
日志保存于稳定存储器中。日志是日志记录的序列,它记录数据库中的所有更新活动。
-
当一个事务Ti开始时,<Ti,start>
-
事务Ti执行write(X)前,日志记录<Ti,X,V1,V2>
V1是write之前X的旧值,V2是需要写入X的新值
-
当Ti结束了最后一条指令时,日志记录<Ti,commit>写入日志
-
当事务Ti中止时,日志记录<Ti,abort>
使用日志的两种方法:
-
延迟的数据库修改(事务提交后还未修改)
直到事务提交时都没有更新到缓冲区/磁盘
- 简化了恢复
- 但是多了本地副本的开销
-
立即的数据库修改(事务提交前已修改)
允许在事务提交前,将未提交的事务更新至缓冲区或磁盘
日志记录的更新必须在数据项被write(数据库修改)之前完成
假设日志记录直接被写入稳定存储器
事务提交
-
当事务将其关于提交的日志记录输出到稳定存储器时,该事务被认为已提交
之前的所有日志记录必须被已经输出
-
事务提交时,由该事务执行的write操作结果可能仍在缓冲区,随后被输出。
并发控制和恢复
在并发事务中,所有事务共享一个磁盘缓冲区和日志,一个缓冲块中的数据项可以来自多个事务的更新
- 如果一个事务Ti修改了一个数据项,那么在Ti提交前,其他事务不能修改同一个数据项(不允许脏写)
- 未提交事务的更新不能被其他事务看见
- 可以通过在被更新数据项上获取排他锁,并持有该锁直到事务提交位置来保证(严格两阶段封锁)
- 不同事务的日志记录在日志中穿插存储
Undo和Redo
- undo(Ti):将事务Ti所更新的所有数据项的值全部恢复成旧值,回到Ti的最后一条日志记录
- 每次数据项X被恢复成旧值,日志记录<Ti,X,V>会被写入
- 当事务的undo操作完成时,日志记录<Ti,abort>被写入
- redo(Ti):将事务Ti所更新的所有数据项的值置为新值,从Ti的第一条日志记录开始执行
- 这个情况下没有任何日志记录
从故障中恢复时:
当日志是以下状态时,事务Ti需要进行undo操作
- 有日志<Ti,start>
- 没有日志<Ti,commit>和<Ti,abort>
当日志是以下状态时,事务Ti需要进行redo操作
- 有日志<Ti,start>
- 有日志<Ti,commit>或<Ti,abort>
如果事务Ti之前执行了undo操作,<Ti,abort>被写入日志,接着故障发生,为了从故障中恢复,Ti要执行redo操作
这样的redo操作重新执行了原先的所有操作,包括重新存储旧值
- 重复历史
- 浪费但简化了恢复
检查点
- 如果系统已经运行了很长一段时间,那么处理整个日志很浪费时间
- 那些已经将输出更新到数据库的事务没必要redo
流线型恢复过程周期性地执行检查点
- 将当前位于主存的所有日志记录输出到稳定存储器上
- 将所有修改了的缓冲块输出到磁盘上
- 将一个日志记录<checkpoint L>输出到稳定存储器
执行检查点时,所有数据更新都停止
恢复时,仅考虑在检查点前最近开始的事务Ti,及在Ti后开始的事务
- 从日志末尾反向扫描,找到最近的<checkpoint L>记录
- 只有在L未提交/中止的事务或者在检查点后开始的事务需要undo或redo
- 检查点之前的已提交或者中止的事务已经将其更新输出到了稳定存储器
undo操作可能需要一些早期的日志
- 继续从日志末尾反向扫描直到找到在L的每个事务Ti的记录<Ti,start>
检查点之前commit的事务无需检查
检查点之后,崩溃之前commit的事务需要redo
崩溃时还未commit的事务需要undo
X的新值
-
当Ti结束了最后一条指令时,日志记录<Ti,commit>写入日志
-
当事务Ti中止时,日志记录<Ti,abort>
使用日志的两种方法:
-
延迟的数据库修改(事务提交后还未修改)
直到事务提交时都没有更新到缓冲区/磁盘
- 简化了恢复
- 但是多了本地副本的开销
-
立即的数据库修改(事务提交前已修改)
允许在事务提交前,将未提交的事务更新至缓冲区或磁盘
日志记录的更新必须在数据项被write(数据库修改)之前完成
假设日志记录直接被写入稳定存储器
事务提交
-
当事务将其关于提交的日志记录输出到稳定存储器时,该事务被认为已提交
之前的所有日志记录必须被已经输出
-
事务提交时,由该事务执行的write操作结果可能仍在缓冲区,随后被输出。
并发控制和恢复
在并发事务中,所有事务共享一个磁盘缓冲区和日志,一个缓冲块中的数据项可以来自多个事务的更新
- 如果一个事务Ti修改了一个数据项,那么在Ti提交前,其他事务不能修改同一个数据项(不允许脏写)
- 未提交事务的更新不能被其他事务看见
- 可以通过在被更新数据项上获取排他锁,并持有该锁直到事务提交位置来保证(严格两阶段封锁)
- 不同事务的日志记录在日志中穿插存储
Undo和Redo
- undo(Ti):将事务Ti所更新的所有数据项的值全部恢复成旧值,回到Ti的最后一条日志记录
- 每次数据项X被恢复成旧值,日志记录<Ti,X,V>会被写入
- 当事务的undo操作完成时,日志记录<Ti,abort>被写入
- redo(Ti):将事务Ti所更新的所有数据项的值置为新值,从Ti的第一条日志记录开始执行
- 这个情况下没有任何日志记录
从故障中恢复时:
当日志是以下状态时,事务Ti需要进行undo操作
- 有日志<Ti,start>
- 没有日志<Ti,commit>和<Ti,abort>
当日志是以下状态时,事务Ti需要进行redo操作
- 有日志<Ti,start>
- 有日志<Ti,commit>或<Ti,abort>
如果事务Ti之前执行了undo操作,<Ti,abort>被写入日志,接着故障发生,为了从故障中恢复,Ti要执行redo操作
这样的redo操作重新执行了原先的所有操作,包括重新存储旧值
- 重复历史
- 浪费但简化了恢复
检查点
- 如果系统已经运行了很长一段时间,那么处理整个日志很浪费时间
- 那些已经将输出更新到数据库的事务没必要redo
流线型恢复过程周期性地执行检查点
- 将当前位于主存的所有日志记录输出到稳定存储器上
- 将所有修改了的缓冲块输出到磁盘上
- 将一个日志记录<checkpoint L>输出到稳定存储器
执行检查点时,所有数据更新都停止
恢复时,仅考虑在检查点前最近开始的事务Ti,及在Ti后开始的事务
- 从日志末尾反向扫描,找到最近的<checkpoint L>记录
- 只有在L未提交/中止的事务或者在检查点后开始的事务需要undo或redo
- 检查点之前的已提交或者中止的事务已经将其更新输出到了稳定存储器
undo操作可能需要一些早期的日志
- 继续从日志末尾反向扫描直到找到在L的每个事务Ti的记录<Ti,start>
[外链图片转存中…(img-yhQWQw5t-1704790792385)]
检查点之前commit的事务无需检查
检查点之后,崩溃之前commit的事务需要redo
崩溃时还未commit的事务需要undo