第四章 数据库管理系统
- DBMS的架构
- DBMS内核
- DBMS运行时的进程结构
- DBMS访问管理
- 查询优化
- 事务管理
4.1 DBMS架构
4.1.1 DBMS内核
4.1.2 DBMS进程结构
单进程结构
应用程序+DBMS内核共同编译为一个.exe,作为一个进程运行。
多进程结构
应用程序通过CONNECT请求连接。
一个应用程序对应一个DBMS内核进程。单机通过管道通信,联机通过socket通信
缺点:操作系统创建进程需要资源,当应用进程较多时,会导致操作系统资源性能急剧下降
多线程结构:轻量级的进程
每一个应用程序对应一个DBMS线程。
- DAEMON:接收连接请求;创建DBMS线程
- catalog 目录:关于数据的数据。(用于语法分析,e.g.库中有某表)
- lock table:控制锁的申请。(用于并发控制)
- buffer:缓冲(查询优化??)
4.2 访问管理
将关系型数据库对象的操作转换成操作系统具体物理对象(文件)的操作。
4.2.1 访问类型
访问类型 | 文件组织 |
---|---|
访问一个文件的大部分元组Query all:访问一个文件的15%以上 | 堆文件 |
查找某条特定元组 | 哈希文件 |
查找部分元组:<15% | |
范围查询 | |
更新 |
4.2.2 文件组织
堆文件:每次想文件末尾写入数据。查找时顺序查找。
Hash文件:适用于查找特定元组
B+树 + 堆文件:对某一属性建立B+树。适用于全部访问类型
4.2.3 索引方法
B+树
簇集索引
倒排索引
动态Hash
4.3 查询优化
4.3.1 查询优化概述
代数优化:将用户提交的查询语句改写为等价的效率更高的形式,操作最少
操作优化:找到最有效的操作实现方法。
4.3.2 代数优化
查询树:叶子节点:关系;中间节点:单表/多表擦做;叶子-根的路径:操作的执行顺序
代数优化的等价变换原则
(1)连接、笛卡尔积的交换律:E1
×
\times
×E2=E2
×
\times
×E1
即:父节点为连接/笛卡尔积的查询树,左右子树可交换
(2)连接、笛卡尔积的结合律:(E1
×
\times
×E2)
×
\times
×E3=E1
×
\times
×(E2
×
\times
×E3)
即:祖父节点为连接/笛卡尔积的查询树,可旋转
(3)投影的串接律:若
A
1
、
A
2
、
.
.
.
、
A
n
{A_1、A_2、...、A_n}
A1、A2、...、An为
{
B
1
、
B
2
、
.
.
.
、
B
m
}
\{B_1、B_2、...、B_m\}
{B1、B2、...、Bm}的子集,则有:
∏
A
1
.
.
.
A
n
(
∏
B
1
.
.
.
B
m
(
E
)
)
≡
∏
A
1...
A
n
(
E
)
\prod_{A_1...A_n}(\prod_{B_1...B_m}(E))\equiv\prod_{A1...A_n}(E)
∏A1...An(∏B1...Bm(E))≡∏A1...An(E)
(4)选择的串接律:
σ
F
1
(
σ
F
2
(
E
)
)
≡
σ
F
1
∧
F
2
(
E
)
\sigma_{F_1}(\sigma_{F_2}(E))\equiv\sigma_{F_1\land F_2}(E)
σF1(σF2(E))≡σF1∧F2(E)
(5)选择、投影的交换律:
若选择条件
F
F
F中只包含投影属性
A
1
.
.
.
A
n
A_1...A_n
A1...An的子集,则有:
σ
F
(
∏
A
1
.
.
.
A
n
(
E
)
)
≡
∏
A
1
.
.
.
A
n
(
σ
F
(
E
)
)
\sigma_{F}(\prod_{A_1...A_n}(E))\equiv\prod_{A_1...A_n}(\sigma_F(E))
σF(∏A1...An(E))≡∏A1...An(σF(E));
若选择若选择条件
F
F
F中包含投影属性
A
1
.
.
.
A
n
A_1...A_n
A1...An的子集也包含不属于投影属性的属性
B
1
.
.
.
B
m
B_1...B_m
B1...Bm,则有:
∏
A
1
.
.
.
A
n
(
σ
F
(
E
)
)
≡
∏
A
1
.
.
.
A
n
(
σ
F
(
∏
A
1
.
.
.
A
n
B
1
.
.
.
B
n
(
E
)
)
)
\prod_{A_1...A_n}(\sigma_F(E))\equiv\prod_{A_1...A_n}(\sigma_F(\prod_{A_1...A_nB_1...B_n}(E)))
∏A1...An(σF(E))≡∏A1...An(σF(∏A1...AnB1...Bn(E)));
(6)选择与多表操作:
若选择条件只是其中一表的属性,可以将选择操作下压。
σ
F
(
E
1
×
E
2
)
≡
σ
F
(
E
1
)
×
E
2
\sigma_F(E1\times E2)\equiv\sigma_F(E1)\times E2
σF(E1×E2)≡σF(E1)×E2;
若选择条件:
F
=
F
1
∧
F
2
F=F1\land F2
F=F1∧F2(其中
F
1
F1
F1只包含
E
1
E1
E1属性,
F
2
F2
F2只包含
E
2
E2
E2属性),可以将选择操作下压。
σ
F
(
E
1
∧
E
2
)
≡
σ
F
1
(
E
1
)
×
σ
F
2
(
E
2
)
\sigma_F(E1\land E2)\equiv\sigma_{F1}(E1)\times\sigma_{F2}(E2)
σF(E1∧E2)≡σF1(E1)×σF2(E2);
若选择条件
F
=
F
1
∧
F
2
F=F1\land F2
F=F1∧F2(其中
F
1
F1
F1只包含
E
1
E1
E1属性,
F
2
F2
F2包含
E
1
、
E
2
E1、E2
E1、E2属性),可以将一个选择操作下压。
σ
F
(
E
1
∧
E
2
)
≡
σ
F
2
(
σ
F
1
(
E
1
)
×
E
2
)
\sigma_F(E1\land E2)\equiv\sigma_{F2}(\sigma_{F1}(E1)\times E2)
σF(E1∧E2)≡σF2(σF1(E1)×E2);
(7)
σ
F
(
E
1
∪
E
2
)
≡
σ
F
(
E
1
)
∪
σ
F
(
E
2
)
\sigma_F(E1\cup E2)\equiv\sigma_F(E1)\cup\sigma_F(E2)
σF(E1∪E2)≡σF(E1)∪σF(E2)
(8)
σ
F
(
E
1
−
E
2
)
≡
σ
F
(
E
1
)
−
σ
F
(
E
2
)
\sigma_F(E1- E2)\equiv\sigma_F(E1)-\sigma_F(E2)
σF(E1−E2)≡σF(E1)−σF(E2)
(9)若
A
1
.
.
.
A
n
=
B
1
.
.
.
B
m
∪
C
1
.
.
.
C
k
A_1...A_n=B_1...B_m\cup C_1...C_k
A1...An=B1...Bm∪C1...Ck,其中
B
B
B为
E
1
E1
E1的属性,
C
C
C为
E
2
E2
E2的属性。
∏
A
1
.
.
.
A
n
(
E
1
×
E
2
)
≡
∏
B
1
.
.
.
B
m
(
E
1
)
×
∏
C
1
.
.
.
C
k
(
E
2
)
\prod_{A_1...A_n}(E1\times E2)\equiv\prod_{B_1...B_m}(E1)\times\prod_{C_1...C_k}(E2)
∏A1...An(E1×E2)≡∏B1...Bm(E1)×∏C1...Ck(E2)
(10)
∏
A
1
.
.
.
A
n
(
E
1
∪
E
2
)
≡
∏
A
1
.
.
.
A
n
(
E
1
)
∪
∏
A
1
.
.
.
A
n
(
E
2
)
\prod_{A_1...A_n}(E1\cup E2)\equiv\prod_{A_1...A_n}(E1)\cup\prod_{A_1...A_n}(E2)
∏A1...An(E1∪E2)≡∏A1...An(E1)∪∏A1...An(E2)
代数优化基本原则
(1)将一元操作尽量下压(减少二元操作数据规模)
(2)组合公共子表达式
4.3.2 操作优化
1.连接操作的优化
嵌套循环改进:关系O作为外层虚幻,关系I作为内层循环,对O中每一元组,扫描I一次检查连接条件。
磁盘阵列以块为单位访存,减少访外存次数是改进的关键。因此在内存开辟一个缓冲区(
n
n
n
B
l
o
c
k
s
Blocks
Blocks),其中1块用于内存循环,n-1块用于外层循环。
【思考】:为什么只分配1个块给内层循环?
外层循环读取磁盘次数影响内层循环必须被遍历的次数。因此尽量减少外层循环读磁盘次数收益相对较高。
假设内存缓冲区大小为6 Blocks(1 Blocks存储10个元组),关系O(1000个元组),关系I(500个元组)。
外层循环5块,内层循环1块:
外层访问磁盘:20次;内层访问磁盘:20*50次
外层循环1块,内层循环5块:
外层访问磁盘:100次;内层访问磁盘:100*10次
归并扫描:两个关系先做外排序,再做连接操作。这样两个关系都只需扫描一次。
适用于:两个关系数据稳定,很少有更新
B+树索引/Hash寻找匹配元组内层循环有关于连接属性的B+树索引/Hash列表,则:内层可以不用循环,而是直接查找内层的索引列表。
【注意】属性重复值的数量>内层关系的20%,用该方法没有优势。原因:>20%时内层关系的块基本都换入缓冲区了,与嵌套循环索引内层没有区别,还多出了维护索引的开销
Hash连接将两个关系一起散列到一个Hash文件中。连接时仅处理Hash文件就能将所有的匹配找到。
适用于:两个关系频繁连接,且数据稳定
4.4 恢复机制
目的:1.减少故障的发生;2.从故障(数据不一致)中恢复:故障后将数据恢复到一致性状态
4.4.1 恢复策略
周期性备份:
变种:备份+增量转储:只备份自从上次备份以来变化的部分。优点:恢复时丢失的数据较少
备份+日志:日志记录上次备份以来数据库所有改变。优点:不会丢失数据
- B.I:老值;A.I:新值
4.4.2 事务
- 事务是数据库运行的基本单位。一个事务一个commit
- 基本原则:原子性、一致性、隔离性、持久性
4.4.3 用于恢复机制的数据结构
commit list:提交列表
active list:运行列表
log:日志
4.4.4 更新策略&故障后恢复
更新:2个规则,3个策略及其对应故障恢复
- 提交规则:commit之前要先将新值A.I(后项)写到硬盘(数据库/日志)上
先记后写规则:更新之前先将老值B.I写到日志中。
redo、undo操作满足幂等性:
undo(undo(undo ... undo(x) ...)) = undo(x)
redo(redo(redo ... redo(x) ...)) = redo(x)
-
A.I写入DB before commit:
重启动恢复:
引入检查点:DBMS中周期性安排一些检查点,检查两个检查点之间:提交成功的TID对DB的修改是否成功;需要回滚的TID对DB的影响是否消除。 -
A.I写入DB after commit:
重启动恢复:
【思考】:A.I写入DB after commit 比 A.I写入DB before commit 效率更高,并发性更好:
因为after commit将修改DB加锁的时机推迟到commit阶段。 -
A.I写入DB并发 with commit:
重启动恢复:
4.5 并发控制
多事务并发运行可能产生的问题:
并发控制的前提条件:
DBMS对并发事务的不同调度会产生不同的结果。
其中与事务s的某种串行化结果相同的调度(可串行化)都是正确的,其他都是错误的。
n个事务并发运行有
n
!
n!
n!种正确调度结果
4.5.1 封锁法
1.X锁:排他锁
基本思想:
对读、更新操作加排他锁,强行串行化。
相容矩阵
两段锁协议(2PL):所有加锁操作(growing阶段)在所有解锁操作(shrinking阶段)之前。
well-formed:对数据库先加锁后操作就是well-formed,否则就不是。
结论:
1.well-formed+2PL:可串行化
仍不能消除数据写-读冲突恢复时的多米诺现象。
2.well-formed+2PL+写的锁在事务结束时解锁:消除多米诺现象
3.well-formed+2PL+所有锁在事务结束时解锁:严格的2段锁协议
2.(S,X)锁:共享锁、排他锁
基本思想:
读+共享锁,更新+排他锁。允许读操作同时进行
相容矩阵
3.(S,U,X)锁:共享锁、更新锁、排他锁
基本思想:
读+S锁;
更新:先加更新锁(允许其他事务读),在要写入DB时加排他锁缩短更新事务对数据对象“排他”的时间
可以与更新策略:A.I写入DBafter commit配合使用
相容矩阵
4.5.2 死锁&活锁
死锁:多个并发运行的事务循环等待
活锁:所有的事务都在有限时间释放资源,但仍有事务在相当长时间内不能获取资源(因为调度问题)
死锁的解决:允许出现,只是解决它
- 超时法:当一个事务等待超过某时间限制,就认为发生死锁,则kill该事务释放其资源。(仅在小系统中使用)
- 等待图法:定义
G
=
(
V
,
E
)
G=(V, E)
G=(V,E):
V
V
V:参与并发的事务;
E
E
E:等待关系。
事务运行时安排后台线程动态维护等待图。当等待图出现环路,说明出现死锁。
死锁时选择一个换路上的事务为牺牲者(选择原则:回滚代价最小。e.g.:最年轻/申请锁最少),释放牺牲者资源,重新启动等待链
死锁的预防
-
❌事务运行前申请所有的锁(不现实:有些事务需要修改几十几千的数据)
-
❌对所有数据排序,事务运行时申请锁必须按照顺序申请(不现实:很难排序)
-
❌并发运行出现访问冲突时直接abort释放资源
-
给每个事务安排时间戳。
- 等死法:发生访问冲突时:比较两事务时间戳。我比你小:kill自己;我比你大:继续等待。
只有年老的等待年轻的,不会产生环路,因此可以预防死锁。
【不产生活锁】:年轻的终会“多年媳妇熬成婆”,不需要kill自己,等待直到自己获得资源 - 击伤等待法:发生访问冲突时:比较两事务时间戳。我比你小:继续等待;我比你大:kill你方。
只有年轻的等待年老的,不产生环路
【不产生活锁】
- 等死法:发生访问冲突时:比较两事务时间戳。我比你小:kill自己;我比你大:继续等待。