【数据库系统】课程笔记 - part 2
写在前面:
成绩
由于 db 的笔记有点太多了(
注:Lecture 1 为导论课,Lecture 7 为期中考试复习课,因此没有记录笔记。
另注:我已经尽可能写的详细了,但仍不保证知识点覆盖完全。
【数据库系统】课程笔记 - part 1:https://www.luogu.com.cn/article/47sozboa
【数据库系统】课程笔记 - part 2:https://www.luogu.com.cn/article/xcqqbg8c
【数据库系统】课程笔记 - part 3:https://www.luogu.com.cn/article/fk3j2hgg
Lecture 6 - Relational Database Design
First Normal Form(一范式)
一个 domain 被称为原子性的 (atomic),如果其包含的信息是不可再分的。
如果一个关系模式
对于关系型数据库,需要所有的关系模式都满足一范式。
对于不满足原子性的 domain,可以进行一些改造:
- 用多个属性来描述
- 分开存储在不同的表中
- 等等
Pitfalls in Relational Database Design
一个优秀的关系型数据库需要我们找到一些 “好” 的关系模式,那么什么样的关系模式是不好的:
- 冗余存储,修改复杂等
接下来介绍如何将不好的关系模式变为好的。
Decomposition
最主要的方法就是分解,例如将
-
不能丢属性,即
R=R_1\cup R_2 -
需要是无损连接分解 (Lossless-join decomposition),即对所有在
R 上的关系实例r ,都要满足:r=\prod_{R_1}(r)\bowtie \prod_{R_2}(r) 例如下图就不是无损连接分解:
Functional Dependencies(函数依赖)
设
如果对任何
并写作:
由以上定义不难得到:
-
K$ 是 $R$ 的超码当且仅当 $K\rightarrow R -
K$ 是 $R$ 的候选码当且仅当 $K\rightarrow R$,且不存在 $\alpha\subset K,\alpha\rightarrow R
Definition of Trivial and Non-Trivial Dependency
有一些函数依赖关系是平凡的 (trivial),这些关系是显然成立 的,例如:
换言之,如果
Closure of a Set of Functional Dependencies
首先是逻辑导出 (logically imply):显然由
接下来定义函数依赖集
Armstrong’s Axioms
给出一些公理,用于求解闭包:
- 自反律 (reflexivity):若
B\subseteq A ,则A\rightarrow B (平凡的) - 增补律 (augmentation):若
A\rightarrow B ,则CA\rightarrow CB - 传递律 (transitivity):若
A\rightarrow B,B\rightarrow C ,则A\rightarrow C
这三条定律是完备的 (complete) 和 保真的 (sound),它们可以精确地得到所有函数依赖(不重不漏不多)
为了方便使用,引申出一些二级公理来加速求解:
- 合并律 (union):若
A\rightarrow B,A\rightarrow C ,则A\rightarrow BC - 分解律 (decomposition):若
A\rightarrow BC ,则A\rightarrow B 且A\rightarrow C - 伪传递律 (pseudotransitivity):若
A\rightarrow B,BC\rightarrow D ,则AC\rightarrow D
那么最暴力的寻找闭包的方法就是:
- 先加入所有使用自反律和增补律能得到的关系
- 再利用传递律得到最终答案
显然最坏情况下会有
Closure of Attribute Sets
属性集的闭包就比函数依赖集的闭包要更容易求一些。
给出属性集
那么:
-
a$ 是一个超码当且仅当 $R\subseteq a^+ -
a\rightarrow b$ 当且仅当 $b\subseteq a^+
上图是一个例子。
Canonical Cover(正则覆盖)
显然,一些函数依赖集中是存在冗余的,因此定义
那么
如何找出正则覆盖:
- 可以被表出的函数依赖是多余的,如
A\rightarrow B,B\rightarrow C,A\rightarrow C - 函数依赖的左侧或右侧存在多余的属性,以右侧为例:
- 如果
B=pq,A\rightarrow B ,则A\rightarrow p,A\rightarrow q ,如果删去A\rightarrow B ,加入A\rightarrow p 之后可以推出A\rightarrow q ,那么q 这一项就是多余的,可以去除
- 如果
上图是一个求解正则覆盖的例子。
Decomposition
在一些情况下,一个关系模式
- 是无损连接分解
- 有依赖保持 (dependency preservation)
- 每个
R_i 都是好的(例如满足 BCNF 或 3-NF)
无损连接分解
定理:设
依赖保持
定理:将
- 记
F_i 为F^+ 中只保留在R_i 中的属性形成的函数依赖集
那么
BCNF / 3-NF
将在之后介绍。
一个例子
例如
- 如果分解为
R_1=(A,B),R_2=(B,C) : -
- 如果分解为
R_1=(A,B),R_2=(A,C) : -
下图给出了一个判定
不断地从
Boyce-Codd Normal Form
称一个关系模式
- 每个在
F^+ 中的函数依赖\alpha\rightarrow \beta 都满足下列二者之一: -
那么
但如果要检查分解式是否满足 BC - 范式,则必须检查
一个分解算法
其思想为:每次找到一条
换言之,满足 BCNF,满足无损连接分解,满足依赖保持三个条件可能无法同时成立。
Third Normal Form
BCNF 的限制太强,以至于可能无法满足依赖保持,因此设计了一种限制稍微弱一点的范式,即 3 - 范式。
称一个关系模式
- 每个在
F^+ 中的函数依赖\alpha\rightarrow \beta 都满足下列三者之一:-
-
- 每个
A\in \beta-\alpha 都包含于某个R 的候选码中
-
那么显然如果满足 BCNF 则一定满足 3 - NF,同时这里的第三个条件是为了能保证依赖保持做出的最低程度的弱化。
与 BCNF 类似,
判断一个关系是否满足 3 - NF 是 NP-hard 的(因为需要计算所有候选码),但进行 3 - NF 的分解是存在多项式复杂度的算法的,下面是一个例子:
- 先求出
F 的正则覆盖F_c - 对每个
F_c 中的函数依赖\alpha\rightarrow \beta ,如果之前分解出的模式中包含了(\alpha,\beta) ,则无需操作,否则新建一个(\alpha,\beta) 的模式 - 最后,如果所有模式都不包含任意一个候选码,则添加任意一个候选码作为一个模式,这样可以保证无损连接分解
- 这一步中只需要找到任意一个候选码即可,原因是如果有模式包含这个候选码,则自然满足无损连接分解,否则添加了一个之后也能满足,找到任意一个候选码是有多项式复杂度的做法的
这样,一定可以得到一个满足 3 - NF,满足无损连接分解,满足依赖保持的分解式。
Multivalued Dependencies
有时在满足 BCNF 的情况下依然会产生冗余,即多值依赖 (Multivalued Dependencies)。
下面是多值依赖的定义:
称两个属性
- 对任意两个元组
t_1,t_2 (t_1[A]=t_2[A] ),则一定存在元组t_3 ,满足t_1[A]=t_3[A],t_1[B]=t_3[B],t_2[R-A-B]=t_3[R-A-B]
换言之,只要属性
Fourth Normal Form
引入 4 - 范式,称一个关系模式
- 每个在
D^+ 中的多值依赖\alpha\rightarrow \rightarrow\beta 都满足下列二者之一: -
分解方法和 BCNF 的分解方法相同。
Lecture 8 - Storage and File Structure
Review
先对之前介绍过的一些内容进行回顾。
Storage Manager
存储管理器 (storage manager) 是一个程序模块,它在数据库中存储的底层数据,与提交给系统的应用程序和查询之间提供了接口。
其负责两项核心的内容:
- 与文件管理器进行交互
- 高效的存储,检索和更新数据
并且其包含以下几个子模块:
- 事务管理器 (transaction manager)
- 授权与完整性管理器 (authorization and integrity manager)
- 文件管理器 (file manager)
- 缓冲区管理器 (buffer manager)
Query Processor
询问处理器包括:DDL interpreter, DML interpreter, query processing,其主要功能为 转义 (parsing and translation),优化 (optimization),执行 (evaluation)。
这两个东西的整体结构如下图所示:
Overview of Physical Storage Media
在物理层面上,数据最终都存储在文件中,不同数据库系统有各自的文件格式,例如 .mdf/.ldf,.ora,.dbf 等等。
不同的存储介质可以用不同的方式进行分类:
- 访问速度:数据被读写所需的时间
- 单位成本:每单位存储空间的价格
- 可靠性:数据的持久化与稳定性,主要考虑:
- 掉电或系统崩溃时是否会丢失数据
- 存储设备本身的物理故障风险(如 RAID 技术用于应对此类问题)
如果按照可靠性分类,则有:
- 易失性存储 (volatile storage):断电后数据会消失,如 DDR2,SDR 等主存(即 RAM)
- 非易失性存储 (non-volatile storage):断电后数据不会消失,如磁盘,SSD,磁带等
如果按照访问速度分类,则有:
- 高速缓存 (cache):速度最快(读写时间
\le \rm0.5ns ,大小在\rm KB\sim MB ),容量极小,成本最高,断电后数据消失 - 主存/内存 (main-memory):速度较快(读写时间在
\rm 10\sim 100ns ),容量较小(几个\rm GB ),断电后数据消失 - 快闪存储器 (flash memory):读的速度与内存相近,但写的速度很慢(在
\rm 10\mu s ) - 磁盘/机械硬盘 (magnetic disk):大容量,低成本,速度较慢
- 数据存储在高速旋转的磁盘盘片上,通过磁头以磁性方式读写
- 它是数据库系统中长期数据存储的核心介质,绝大多数数据库的主数据文件都存放在机械硬盘上
- 数据访问分成两个步骤:先从磁盘读取到内存中;修改完成后,再写回磁盘进行持久化存储
- 虽然磁盘物理故障可能导致数据损坏,但这类故障概率很低
- 光盘 (optical storage)
- 磁带 (tape storage):速度最慢,但容量极大,成本极低,多用于离线备份
Magnetic Disks
首先给出一张机械硬盘的结构示意图:
解释一下这张图中的各个部分:
- 读写头 (read-write head):几乎贴近盘片表面,距离极近但不接触,以磁性方式读取/写入盘片上的数据
- 盘片 (platter) 表面被划分为一圈圈的同心圆,被称为磁道 (track),一般的硬盘一个盘片上磁道的数量可以达到
50000\sim 100000 条 - 每个磁道又被分为多个扇区 (sector),这是磁盘读写的最小单位(一次必须至少读写一整个扇区),大小通常为 512B(也有 4KB 的扇区格式),单条磁道上扇区的个数在
500\sim 2000 个,随同心圆的周长增大而变多
那么不难得到读写一个扇区的方式:
- 寻道 (seek):磁盘臂摆动,将读写头移动到目标磁道上,普通磁盘约
4\sim 10\rm ms ,这个是磁盘访问的速度瓶颈 - 旋转等待 (rotational latency):盘片持续旋转,直到目标扇区转到读写头下方,这个时间根据磁盘的转速来确定,通常在
4\sim 11\rm ms - 所以磁盘的 access time 就是 seek time 加上 rotational latency time
- 数据传输:扇区经过读写头时,完成数据的读取或写入
一个硬盘通常包含
柱面 (cylinder) 是指所有盘片上编号相同的磁道,那么显然同一柱面内的数据读写无需移动磁盘臂,效率更高。
磁盘控制器 (disk controller) 是主机与硬盘硬件之间的接口,主要负责:
- 指令执行:接收主机读写扇区的高级指令,控制磁盘臂移动到目标磁道、完成数据读写
- 数据校验:为每个扇区计算并附加校验和 (Checksum),读取时验证数据是否损坏
- 写后验证:写入数据后,立即重读扇区,确保写入成功
- 坏扇区重映射:当检测到物理损坏的扇区时,会将其逻辑地址映射到磁盘预留的备用扇区中,并记录在非易失性存储中,实现对主机透明的故障容错
磁盘传输率 (data-transfer rate) 指的是磁盘在找到数据后,传输数据的速率,典型机械硬盘的最大传输速率约为
平均故障时间 (mean time to failure, MTTF) 指的是磁盘连续工作不出问题的平均时间,典型的机械硬盘的 MTTF 在 3 至 5 年,并且随着硬盘使用时间增长而逐渐降低。
Optimization of Disk-Block Access
-
磁盘块 (block):一个磁道上的连续若干个扇区组成的一个整体,在硬盘和内存进行数据传输时,以一个 block 为一个基本单元,通常一个 block 的大小在
512\rm B 至几\rm KB 。 -
设计一些算法优化磁头的移动路径,例如电梯算法 (elevator algorithm)。
-
文件组织 (file organization):根据数据访问的模式来规划 block 的存储位置,从而减少访问时间。
- 例如将有关联的数据放在同一个柱面或者相邻的柱面上,这样可以减少磁头的移动,降低寻道时间
- 磁盘碎片 (fragmentation) 问题:即频繁的插入删除操作会使得原本连续的块变得不连续,从而在读写时需要在不同的磁道之间频繁移动,增加了寻道时间
- 为了解决这个问题,可能需要进行磁盘碎片整理 (defragmentation),将分散的块重新整理到连续的物理空间中,这种做法会占用极高的系统资源,此时系统基本无法正常使用
- 注意,碎片整理仅对机械硬盘 (HDD) 有效,对固态硬盘 (SSD) 没有意义,甚至可能缩短 SSD 的寿命
- 非易失性写缓冲区 (Nonvolatile write buffers):在进行磁盘写入时,不直接写磁盘,而是先被立即写入到非易失性 RAM 缓冲区(系统立刻就能确认写入完成,无需等待磁盘的机械操作),接着磁盘控制器会在磁盘空闲时,再把缓冲区里的数据批量写入磁盘。同时,缓冲区可以暂存多个写请求,然后按照电梯调度算法重新排序,减少磁盘臂移动距离。
- 在软件方面,也有日志盘 (log disk) 这种技术,实现起来类似非易失性 RAM。
RAID
RAID 的全称为独立磁盘冗余阵列 (Redundant Arrays of Independent Disks, RAID),它是一种磁盘组织技术:
- 管理大量磁盘,对外提供一个统一的 “虚拟磁盘” 视图
- 其优势在于:
- 高容量与高性能:通过多磁盘并行读写,提升整体带宽和速度
- 高可靠性:通过数据冗余存储,单块磁盘故障时数据仍可恢复
冗余 (redundancy):存储额外的信息,当某块磁盘发生故障时,可以利用这些额外信息重建丢失的数据,避免数据丢失
典型实现之一:磁盘镜像 (mirroring/shadowing)
- 为每一块物理磁盘创建一个完全相同的副本,逻辑上一块盘由两块物理盘组成
- 每次写入操作会同时写入两块磁盘;读取时可以从任意一块磁盘读取,还能分担读负载,提升性能
- 此时定义 平均修复时间 (MTTR) 和 数据丢失平均时间 (MTTDL),公式为:
\rm MTTDL=\dfrac{MTTF^2}{2\times MTTR} - 镜像方法的核心优势是极高的可靠性和读性能,但缺点是磁盘利用率只有 50%,成本翻倍
磁盘系统并行性的两大目标:提升吞吐量;降低相应时间
- 比特级拆分 (Bit-level striping):将一个字节的不同位分散存储到不同的磁盘上,读这个字节时所有磁盘同时读
- 这种做法的优势是理论上
n 块磁盘可以得到n 倍的读写速度,但缺点是只要有任意一块磁盘故障则整个数据无法恢复,因此现在基本不使用
- 这种做法的优势是理论上
- 块级拆分 (Block-level striping):将文件按 “块” 为单位拆分,不同的块分散存储到不同的磁盘上,这是现代 RAID 技术(如 RAID 0、RAID 5)中最常用的条带化方式
RAID Levels
RAID 0
块级条带化存储,无任何数据冗余备份,数据被拆分分散写到多块磁盘。
特点:读写并行度最高、性能最强,但只要任意一块磁盘故障,对应数据就会永久丢失。
适用场景:高性能优先、数据丢失风险可接受的业务,比如临时缓存、可重新生成的非核心数据存储。
RAID 1
磁盘镜像,每一份数据都完整存放在两块互为镜像的磁盘中。
之前已经介绍过了,不再复述。
RAID 2
采用比特级数据拆分,借鉴内存的 ECC(错误纠正码)机制,把数据按位分散到多块数据盘,同时用多块专用磁盘存储 ECC 校验码来实现错误纠正。
RAID 3
只需要 1 块专用奇偶校验盘就能完成错误纠正,且系统能定位故障磁盘。
写入数据时:同步计算对应奇偶校验位,写入专属校验盘
磁盘故障恢复:对其余所有正常磁盘(含校验盘)的比特做异或运算,就能还原故障盘丢失的数据。
RAID 3 可以实现 RAID 2 全部的容错收益,但只需要 1 块校验盘,硬件成本更低,直接替代了 RAID 2 的应用场景。
RAID 4
采用块级条带化存储,将数据按块分散存储在
读操作优于 RAID 3,但是写操作极差,因此基本被淘汰。
RAID 5
RAID 5 是 RAID 4 的改进版,采用分布式奇偶校验:不再把校验块集中存放在单块盘上,而是将数据块和奇偶校验块分散存储在所有
其解决了 RAID 4 的写瓶颈:由于校验块分散在不同磁盘上,多个写请求可以并行处理,避免了单块校验盘成为性能瓶颈。
RAID 6
在 RAID 5 的基础上,存储两份独立的校验信息,这意味着阵列中可以承受两块磁盘同时故障,仍能通过两份校验信息恢复数据,但相应地磁盘利用率进一步降低。
因此,RAID 6 主要用于对数据安全性要求极高的场景,应用不如 RAID 5 广泛。
Choice of RAID Level
选择 RAID 级别需要考虑的因素有:成本,正常性能,故障时性能,重建性能。
- RAID 0 用于数据安全性不重要的情景
- RAID 2 和 RAID 4 不会使用,因为它们分别被 RAID 3 和 RAID 5 覆盖
- RAID 3 通常不会使用,因为它综合不如 RAID 5
- RAID 6 通常不会使用,因为 RAID 5 已经能够保证安全性
因此主要的选择在 RAID 1 和 RAID 5 之间,而这两者的选择:
- RAID 1:写性能更优,每次写入仅需对两块镜像盘执行 2 次块写入,没有额外的校验开销
- RAID 5:写性能存在 “写惩罚”,每次写入需要执行 2 次读 + 2 次写(读取旧数据块和旧校验块,计算新校验块,再写入新数据块和新校验块),写操作开销是 RAID 1 的两倍以上
- 因此 RAID 5 适合更新率低、数据量大的场景(如文件归档、备份数据),以容量利用率为优先,而其他的情况都可以选择使用 RAID 1
- 同时,在现代硬件条件下 RAID 1 的成本劣势已不再明显(也有 RAID 10,即 RAID 1 + RAID 0)
Hardware Issues
上述内容都是硬件层面的 RAID,也有软件层面的 RAID,其完全由操作系统软件实现,没有专用硬件支持,但会占用主机 CPU 资源,性能和可靠性依赖于系统。
硬件 RAID 也会有一些问题,例如断电可能导致数据损坏,在这种情况下,系统恢复供电后,必须能检测到损坏的数据并修复它。
- 潜伏故障 (latent failures):之前成功写入的数据,在后续使用中被无声损坏(如磁盘坏道),这种故障不会立刻被发现,但如果此时再发生一块磁盘故障,RAID 的冗余机制可能无法恢复数据,导致永久数据丢失
- 数据清洗 (data scrubbing):主动、持续地扫描所有数据块,检测潜伏故障,一旦发现损坏块,会自动利用镜像副本或奇偶校验信息恢复数据
- 热插拔 (Hot Swapping):在系统运行过程中直接更换故障磁盘,无需关机或停机
- 热备盘 (Spare Disks):系统中预先配置一块或多块在线备用磁盘,一旦检测到某块磁盘故障,RAID 控制器会立即自动使用热备盘开始重建数据
Storage Access
数据块 (block):数据库文件会被逻辑上划分为固定长度的存储单元,也就是块 (Block),它同时是数据库系统中存储分配和数据传输的基本单位。
缓冲区 (buffer):缓冲区是主存 (RAM) 中用来存放磁盘块副本的一块区域。
由于磁盘访问速度远低于(大约
这里对三个概念做一个区分:
- page:逻辑上的数据单元
- block:物理上的数据单元
- frame:缓冲区上的数据单元
在这里近似认为 page = block。
缓冲区管理器 (buffer manager):负责在主存中分配、管理缓冲区空间的子系统。它要解决的核心问题是:缓冲区的大小是有限的,当缓冲区被占满、又需要加载新的磁盘块时,需要用特定的替换策略(如 LRU(最近最少使用), MRU(最近最多使用), Clock 等算法)来决定哪些块需要被 “淘汰” 写回磁盘。
-
当程序请求某个内存块时:
-
如果在缓冲区中命中,则直接返回内存地址(buffer 是在内存中的)
-
如果没有命中,则需要申请一个 frame
-
如果没有空余的位置,则需要丢弃一个已在缓冲区中的 frame,丢弃时需要检查是否为脏块(需要额外维护一个脏位的数据),如果是则需要写回到磁盘中,否则直接丢掉即可
申请到 frame 了之后,从磁盘中读出信息,并返回这个 frame 的内存地址
-
这里再引入 pin ("钉住") 的概念:
- pinned block:不允许被写回磁盘、也不允许被替换出缓冲池的内存块,这样可以防止数据被中途替换,保证操作的原子性和数据一致性
- pin count:记录这个缓冲块被 pin 了多少次
File Organization
整个数据库以文件 (file) 的形式存储,每个文件以若干个记录 (record) 构成,每个记录有一系列的域 (field)(即各个属性)。在实际应用中,存在定长记录 (Fixed-length record) 和不定长记录 (Variable-length record) 两种存储方式。
Fixed-Length Records
优点是实现起来非常容易,第
但注意可能出现一个 record 横跨两个 block 的情况,因此需要作出修改,禁止这种情况出现。
删除(假设是第
- 将
i+1\sim n 整体移动到i\sim n-1 - 将
i 和n 交换 - 将删除的 record 加入到 free list 中(记录哪些位置是被删掉的),这里 free list 就是一个链接了所有已删除位置的链表
Variable-Length Records
变长记录的出现有以下几种原因:
- 在一个文件中存储多种不同类型的记录
- 记录类型允许了不同长度的内容(如
varchar) - 可能有重复的域(属性)
每个变长记录有两个部分:<offset,length> 和实际内容,其中 offset,length 分别指定了实际内容在文件中的开始位置和长度。
这种存储方式被称为 slotted page structure,它包含两个部分:
- header,又分为三个部分
- 指示有多少个 record
- header 的结束位置
- 每个 record 的
offset,length
- 实际存储的数据
那么在删除的时候就可以移动这些存储的数据(同时也要修改 offset),来填补形成的空位。同时指针不应该直接指向 record 的数据,而是应该指向在 header 中的标识。
Organization of Records in Files
文件组织形式有以下几种:
- 堆文件 (Heap file):只要有存储空间,可以放在任何地方
- 顺序文件 (Sequential file):按照 search key 以一定顺序存放所有的 record(需要定期为文件重新排序)
- 散列文件 (Hashing file):通过哈希函数来确定每个 record 应该被放在文件的哪个 block 中
- 聚集文件组织 (Clustering file organization):在同一个文件中存储来自不同关系的 record
Lecture 9 - Indexing and Hashing
Basic Concepts
为什么我们需要索引 (index):避免全表扫描,具体来说可以类比字典中的目录,可以根据偏旁部首来快速地找到需要的汉字,而不用把整本字典翻一遍。
索引的相关性质:
- 搜索键 (search key),即用来查找记录的属性(可以有多个)
- 每条索引记录包含两个部分:搜索键和指向真实数据的指针(即偏旁与这个偏旁对应的页码)
- 通常来说索引文件的大小远小于原始数据文件
- 搜索键的排列方式有两类:
- 顺序索引 (ordered indices),即搜索键按存储顺序是有序的,这种方式更有利于范围查询
- 散列索引 (hash indices),使用哈希函数来生成均匀分布的搜索键,这种方式更有利于精确查询,但难以范围查询
- 评估索引的性能有如下几个方面:
- 支持的查询类型,例如等值(精确)查询以及范围查询等
- 索引访问时间
- 索引维护时间(包括插入,删除等)
- 索引存储空间
Ordered Indices
顺序索引 (ordered indices):有序索引中的索引项按 search-key 排序存储
顺序文件 (sequentially ordered file):数据文件本身也是按 search-key 排序的
主索引 (primary index):主索引的 search-key 与顺序数据文件的排序字段完全相同,通常但不一定是主码(只需要有序就可以了),同时,主索引也可以被称为聚集索引 (clustering index),这是因为相邻的索引在物理存储上也在相邻的位置。另外,非顺序文件不存在主索引。
- 稠密索引 (dense index):每个 search-key 的值都有一个对应的索引项(注意不是每一个数据文件中的元组),这种索引方式可以让查找非常快,但索引的更新比较慢
- 稀疏索引 (sparse index):将 search-key 的值分为若干个块 (block),对每个块建立一个索引项,这样查找和更新的时间可以更平衡一些(相当于一个两层的 B+ 树结构,那么不难延伸至多层结构,此时就出现了多层索引 (multilevel index),即将上层的索引内容直接视为数据文件再建立一遍索引)(注意,稀疏索引只能用于顺序文件,而稠密索引可以用于非顺序文件,尽管仍然无法范围查询)
辅助索引 (secondary index):辅助索引的 search-key 顺序与数据文件的物理排序方式不同,因此也被称为非聚集索引 (non-clustering index),在辅助索引上进行扫描的开销是非常大的,因为可能涉及很多的磁盘 IO,产生这种索引是因为用户可能需要对其他的属性进行筛选。
- 辅助索引必须是稠密索引,因为它不是有序的,无法用稀疏索引的查找方法
- 由于一个 search-key 可能会对应多条数据,因此需要一个桶 (bucket) 来存所有对应数据的指针
数据删除
对于删除的情况,首先删除数据库表中的对应内容,接着更新索引的方式如下。
如果是稠密索引
- 如果删除的 search-key 只有这一条记录了,那么就删除这个索引项即可
- 否则,对于辅助索引的情况,删除对应 bucket 中的这个指针,而对于主索引的情况,判断一下删除的内容是不是这类 search-key 的开头元素,如果是的话就更新一下指针,否则什么都不用做
如果是稀疏索引(那么它就不可能是辅助索引)
- 如果删除的 search-key 在索引中没有出现过,那么什么都不用做
- 否则,如果 block 中的下一个 search-key 存在,那么更新到下一个,否则将这个索引项删除
上面说的是对于一层的索引,多层索引就每一层套用同样的做法即可。
数据插入
对于插入的情况,先根据索引找到插入的位置,然后更新索引的方式如下。
如果是稠密索引
- 如果是全新的 search-key,那么将这一项加入索引中
- 否则,对于辅助索引的情况,添加对应 bucket 中的这个指针,而对于主索引的情况,什么都不用做(默认插在同种 search-key 的最末尾)
如果是稀疏索引
- 如果产生了新的 block,那么将对应的数据加入索引中
- 否则,看一下插入的内容是否是 block 中最小的,如果是则需要更新索引
多层索引的情况同理。
B+ - Tree Index Files
这部分介绍数据库系统中的 B+ 树,它可以用来管理顺序索引,下面先给出一个例子:
接着给出 B+ 树的一些性质(这里的 B+ 树和 ADS 中的 B+ 树基本是相同的):
- 从根节点到每个叶节点的路径长度都是相同的
- 每个内部节点(非根非叶节点)都有
\lceil n/2\rceil\sim n 个儿子,其中n 是一个指定的参数 - 每个叶节点都有
\lceil (n-1)/2\rceil\sim n 个值 - 有一些特殊情况,例如当树至少有两层时根节点必须要有至少两个儿子,或者当树只有一层时根节点可以包含
0\sim n-1 个值
B+ 树节点的结构
一个典型的 B+ 树结构如下,其中
B+ 树叶节点
将所有叶节点中的
每个叶节点都有
B+ 树非叶结点
这里夹在
所有非叶结点构成了一个多层索引。
那么如果一共有
下面再给出一个例子:
B+ 树上的操作
查询
比较简单,不作介绍
插入
先在树上进行一次查询找到需要插入的叶节点,如果还有空位则直接插入即可。
否则,需要进行分裂,左侧大小为
注意,这里和 ADS 中的定义不同,因为指针的个数比元素个数多一个:
例如
再插入一个
这张图片展示的是叶子结点分裂,父节点还未分裂的情况,一般的 B+ 树会将父节点分为两半,一边是
更上一级的情况不再重复展示。
或者使用课件里的例子,其实是一样的:
删除
与插入相反,可能会涉及到节点的合并,这里不再展开。
B - Tree Index Files
简而言之,课件中描述的 B 树与 B+ 树的核心区别就是:
- B+ 树是 leafy 的,而 B 树不是
这就导致了 B 树的树高会小一点,但插入删除等操作会变得复杂很多,总体来说不如 B+ 树。
Static Hashing
这部分介绍一下静态哈希 (static hashing):
- 使用哈希函数将 search-key 映射到桶的地址,注意可能会有多个值映射到同一个地址
- 这样就可以直接进行插入/删除/查找(查找时需要遍历桶中的所有指针)
- 注意这种方式不适用于范围查询,只适用于精确查询
注意到实际操作的复杂度开销与一个桶中的指针个数相关,因此哈希函数需要让 search-key 的映射值尽可能均匀分布。
同时还有另一个问题:桶溢出,在数据库系统中,解决这个问题的常用方法是加一个桶,连接到溢出的桶之后,如下图:
再区分一下 Hash File Organization 和 Hash Index,前者是使用哈希方法直接进行数据存储,在桶中存放的就是数据;而后者属于索引,在桶中存放的是指针。
静态哈希中,桶的数量固定不变,因此随着表中数据的增加,一定会使 bucket 越来越满或者发生 overflow,从而导致整体操作效率降低。
Dynamic Hashing
使用动态哈希的方法可以有效处理上面静态哈希的一些缺陷。
动态哈希的核心思想就是先通过哈希函数生成一串较长的
具体来说:
- 需要记录一个全局截取长度
i - 每个 bucket 需要记录一个对应的
i_j 表示这个块中的记录的高i_j 位都是相同的 - 需要一个大小为
2^i 的表格,用来记录每种高i 位的状态对应了哪个 bucket
因此在插入时发生了溢出:
- 如果插入块的
i_j<i ,那么分配一个新的 bucketj' ,并更新原来指向第j 个 bucket 的内容有一半需要指向j' ,接着将原来第j 个 bucket 中的内容删除并重新插入一次(如果还有溢出则需要反复做这个步骤) - 如果插入块的
i_j=i ,那么将i 增加1 ,同时更新表格的信息,然后就变成了上面一种情况
Write-optimized indices
Log Structured Merge (LSM) Tree
现在只考虑插入和查询操作,为了尽量降低磁盘操作的开销,引入 LSM Tree:
每个
初始插入的数据都放入
但需要注意这种结构的删除和修改比较困难,同时查询也需要在多个数据结构中寻找,效率也有所降低。
Lecture 10 - Query Processing
Basic Steps in Query Processing
先介绍一下数据库进行操作的基本步骤:语义分析 - 优化 - 执行(这里 Evaluation 并非评估而是执行的意思)
- Parsing and translation(语法分析与翻译):这个步骤是将输入的 SQL 指令转换为系统能够理解的形式,即 Extended Relational Algebra (ERA)
- Optimization:通过合适的优化方法,生成一个 evaluation-plan,包括:
- 可能可以将输入的指令转化为等价的,更高效的其他指令
- 需要确定扫描方法,是根据索引来找,还是朴素的线性查询
- Evaluation:根据生成的 evaluation-plan 进行实际操作
这里 Optimization 中确定哪种方案更优,就需要对不同的方案进行评估,通过设置合适的代价函数来进行计算:
- 根据使用的具体算法和执行方案来确定某种操作的代价
- 根据数据库系统的统计结果来确定某种操作的代价
Measures of Query Cost
使用 cost 来估计整个查询所需要的时间,具体包含三个部分:
- 磁盘访问时间
- CPU 运行时间
- 网络连接时间
其中磁盘访问时间占大头,因此主要分析这部分(在实际软件中另外的部分也是被计入的)。
为方便起见,假设:
-
-
- 那么
b 次块传输加上S 次寻道所需的时间(预估)为b\times t_T+S\times t_S
Selection Operation
先来考察 select 语句的代价如何估计。
Algorithm 1:线性扫描
在不使用索引的前提下对整个数据库表进行线性扫描,一次测试每条数据是否满足 select 条件,这样:
- 在最开始需要
1 次寻道 - 顺序扫描需要传输
b_r 个块(假设这张表在磁盘中被存在连续的b_r 个块中)
因此估计的代价为
这种算法是最暴力的,对所有的 select 条件/数据存储顺序/是否有索引 的情况都适用。
Algorithm 2:二分查找
在进行等值查找的时候可以使用二分的方式来加速(例如 select score=60 这种)
那么整个过程分成两个部分:
- 先二分查找到分界点所在的 block,这里需要进行
\lceil \log_2 b_r\rceil 次寻道和块传输,因此代价为\lceil \log_2 b_r\rceil\times (t_S+t_T) (因为相当于是随机访问,每次判断都需要一次寻道和一次块传输) - 然后再遍历所有满足条件的 block,假设
sc(A,r),f_r 分别为满足选择条件的数据个数以及每个块存储的数据个数,那么就需要再额外扫描\lceil sc(A,r)/f_r\rceil-1 个块,因此代价为(\lceil sc(A,r)/f_r\rceil-1)\times t_T
Algorithm 3:使用主索引查找唯一值,等值查找
如果需要查找一条满足条件的数据,那么代价为:
Algorithm 4:使用主索引查找重复值,等值查找
此时可能有多条数据都满足条件,那么就是
Algorithm 5:使用辅助索引查找重复值,等值查找
首先在索引树上定位查找的内容,这部分代价也是
但是由于辅助索引内容和实际数据存储顺序可能不同,因此在最坏情况下会有
上图就是一个例子,在 balance 上有一个辅助索引,但是原始数据文件中 balance 是乱序的,那么就可能导致每次查找都需要一次寻道和块传输。
范围查询的情况
和等值查询差不多,如果是
使用主索引和辅助索引的情况和上面基本是相同的,辅助索引仍然可能开销巨大。
Sorting
需要用到排序的情况通常有:
- 输出查询的结果
- 加速 join 操作的运行时间
同时,如果整个表可以被放入内存,那么诸如快速排序等排序算法可以高效地解决,否则需要外部排序等技术。
快速排序不作介绍,因为内存中的操作都比较快,这里主要分析外部排序:
外部排序
设内存中一共有
- 每次读入表中的
M 个 block,在内存中快速完成排序操作并形成一个 run,设一共有N 个 run - 接下来,如果
N<M ,则可以直接进行多路归并,否则,每次将M-1 个 run 合并成一个更长的 run,那么不难发现每次会使 run 的个数除以M-1 ,至多进行\lceil \log_{M-1}(b_r/M)\rceil 轮
代价分析
- block transfer:一共会有
\lceil \log_{M-1}(b_r/M)\rceil 轮,每一轮包含 read/write 两次传输,每轮需要传输所有b_r 个块,最后一轮只考虑 read 不考虑 write,因此 block transfer 次数为b_r(2\lceil \log_{M-1}(b_r/M)\rceil+1) - seek:(*看不太懂)结论是
2\lceil b_r/M\rceil+\lceil b_r/b_b\rceil(2\lceil \log_{M-1}(b_r/M)\rceil-1) ,其中b_b 为缓冲区大小,即一次读/写b_b 个 block
Join Operation
这里介绍五种不同的连接方法。
Nested-Loop Join 嵌套循环连接
其伪代码为:
for each tuple t_r in r do
for each tuple t_s in s do
test (t_r,t_s) and add t_r*t_s into the result
那么,在最坏情况(内存只能容纳每个表的
- block transfer:
n_r\times b_s+b_r ,外层循环一共b_r 次,每次外层循环都要完整遍历一遍s 表,需要b_s 次 - seek:
n_r+b_r ,外层循环一共b_r 次寻道,内层由于是全表扫描,每轮循环只需要一次 seek 即可
最好情况下(
- block transfer:
b_r+b_s ,都全部传输一遍即可 - seek:
2 ,两张表各寻道一次即可
Block Nested-Loop Join
其伪代码为:
for each block B_r in r do
for each block B_s in s do
for each tuple t_r in B_r do
for each tuple t_s in B_s do
test (t_r,t_s) and add t_r*t_s into the result
那么同样在最坏情况下:
- block transfer:
b_r\times b_s+b_r ,这里内部两层循环没有块传输,因此相较于上一种算法有了优化 - seek:
2\times b_r ,同上,将n_r 都优化到了b_r
最好情况下(
- block transfer:
b_r+b_s ,都全部传输一遍即可 - seek:
2 ,两张表各寻道一次即可
中间情况(一共可以容纳
- block transfer:
\lceil b_r/(M-2)\rceil\times b_s+b_r - seek:
2\times \lceil b_r/(M-2)\rceil
Indexed Nested-Loop Join
如果 join 为等值连接或者自然连接,同时内表有连接项上的索引,那么可以用索引优化寻找可以连接的 pair 的时间。
估计总代价为
Merge-Join 排序归并连接
只能用于等值连接或者自然连接,并假设两个表已经针对排序项排好序了。
具体的连接方式和二路归并基本相同,需要
如果初始时无序的,则需要再加上排序的代价。
Hash-Join 哈希连接
只能用于等值连接或者自然连接,使用哈希函数将所有的 tuple 分成若干类,只有在同一类中的 tuple 才可能连接上。
- 先将
s,r 表进行一次哈希,将每个 tuple 映射到0\sim n 中的整数,这样形成若干个 partition - 接着对每个 partition,先将
s_i 读入进内存并用另一个哈希函数再做一次映射,接着一次读入r_i 并利用哈希值进行检查,如果成功则将连接结果加入输出,注意r_i 无需整个放入内存中 - 这里
n 的取值通常为\lceil b_s/M\rceil \times f ,f 为 fudge factor(修正因子)通常取1.2 - 这里
s 也称 build input,r 也称 probe input
代价分析
(*看不太懂)
Other Operations
- Duplicate elimination:可以使用 hashing 或者 sorting 完成
- Projection:循环吧
- Aggregation:类似 Duplicate elimination
- Set operation(交并补):可以使用 hashing 或者 sorting 完成