DBMS Implementation 笔记 02: Representation and operations in PostgreSQL

Tuple Representation

再次强调:一个数据库 (Database) 中的表格 (Relations/Tables) 用一个或多个数据文件 (Data File) ,每个文件由一系列 pages/blocks 组成,每个 page/block 包含多个元组/记录 (tuples/records)。

但实际上,tuple 和 record 还是存在一定区别的。本节就需要对这种区别进行了解。首先是 tuple,tuple 实际上基于 schema 的一系列属性值 (Attributes):

在这里插入图片描述
而 record 则是一个由字节组成的序列,包含来自一个 tuple 的所有数据:

在这里插入图片描述
records 中的字节需要相对于模式 (Schema) 进行解释以获得 tuple。所以,record 就是存储于 page 中的实际数据,而 tuple 使我们作为用户希望看到的数据呈现的形式。

在这里插入图片描述
通过从磁盘 (Disk) 中读取包含 record 的 page,得到我们想要的 record,将其读入内存 (Memory),然后将其释译为 tuple。这就需要一个合适的释译方式,而进行释译的信息可能会被存储在各种地方,比如:

  • 包含在 DBMS catalog 的 schema 数据中(Fixed-size Record)
  • 存储在 page directory 中
  • 存储在 record header 中
  • 一部分存储在 record header,一部分存储在 schema 中

对于变长 record (variable-length record),需要将一部分信息存储在 record 或 page directory 中,比如某个 attribute 的类型为 varchar(20),这就会使得每个 record 的尺寸不定,那么我们就需要将这种信息记录在和 record 直接相关的某些地方,page directory/record 是很直觉的选择。

现在,我们就知道了从数据库中使用某个 tuple 的具体过程:首先从磁盘中读取包含所需 record 的 page,然后使用 mkTuple(rel, rec) 将 record 释译为 tuple。之后使用 getTypField(Tuple i, int t) 来获取其中的各 attribute 值。

Fixed-length Records

对于定长 records,一种比较可能的格式如下图所示:
在这里插入图片描述
即:

  • Record 的格式信息存储在 catalog 中,格式为 (length, offset1, offset2, …)
  • Record 的具体数据存储在某个 page 中

由于格式信息 (Format Info) 需要经常使用,所以缓存在内存中

Variable-length Records

对于变长的 records,有 3 种方式在其中表示每个 attribute 的起止:

  1. 用一个前缀来表示每个 attribute 的长度:
    在这里插入图片描述

  2. 用一个后缀来表示每个 attribute 的终止:
    在这里插入图片描述

  3. 用一个 offset 数组放在开头来表示(这里第一个 offset 为 10 是因为 header 中存储了 5 个数字,每个数是 2 Byte,因此占用了 10 Bytes,所以第一个 attribute 从 10 开始):
    在这里插入图片描述

下面具体来看 tuple 的结构。tuple 实际上一个由多个字段描述器 (Field Descriptor) 组成的列表,它包含:

  • 一个字段描述器列表,FieldDesc 它给出 (offset, length, type) 信息
  • 指向 record 实例数据的指针

在这里插入图片描述
具体形式如下图所示:

在这里插入图片描述
注意,这里的 data_off 表示指向实例数据的指针的位置,而 fields 中的 offset 指的是在实例数据 (record) 中,每个 attribute 的起始位置

当然也可以直接将 tuple data 添加在 tuple structure 中:

在这里插入图片描述

PostgreSQL Tuples

PostgreSQL 通过以下几个要素实现 tuple:

  • 连续的内存块 (a contiguous chunk of memory)
  • 以 header 开头 (header 包含字段的数量等)
  • 接着是数据值 (一个数据序列)

HeapTupleData 包含一个存储的 tuple 的所有信息。

在这里插入图片描述
具体的结构如下图所示:

在这里插入图片描述
简单的来说,每个 tuple = header + data。 tuple 的具体结构如下:

在这里插入图片描述
这里解释一下什么是 command ID,每个事务 (Transaction) 都由一系列 command 组成,其中进行了删除操作的 command 的 ID 被记录下来,之后留下一个指向新的 tuple 版本的指针。所以上述结构中的前三项决定了 MVCC 下 tuple 的可见性

Relational Operations

回顾一下 DBMS 的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KliyOVMl-1620641090092)(E:\课程文件\COMP9315\Week 3\Note\Relational Operations.bmp)]

我们目前为止已经了解了如何从磁盘获取数据,以及在 buffer 中如何管理这些获取的数据(即 Disk/File Management 和 Buffer Management)。现在我们来关注 Access Methods 和 Relational Operators。

Relational Operations

回顾一下 DBMS 的结构:

在这里插入图片描述
我们目前为止已经了解了如何从磁盘获取数据,以及在 buffer 中如何管理这些获取的数据(即 Disk/File Management 和 Buffer Management)。现在我们来关注 Access Methods 和 Relational Operators。

我们已经知道,DBMS 的核心就是一个关系引擎 (Relational Engine),用以实现 SELECTION, PROJECTION, JOIN, SCANNING, SORTING 等逻辑操作。现在我们需要把目光放在检查实现每个操作的方法,为每个实现构建一个成本模型 (Cost Model) 以及确定每种方法最有效的时间上 (Query Optimizer) 。

再次回顾一下几个专业术语:

  • Tuple:服从某些模式 (schema) 的数据值集合 ≈ Record
  • Page = Block:一系列 Tuples + Management Data,这是最小的读/写单位
  • Relation = Table: 由一到多个 File 组成,File 是一系列 Tuples 的集合

每当我们进行一次查询 (Query) 时,实际上是从两个维度定义了查询方法 (Query Method):

  1. 关系逻辑操作 (Relational Operation):比如是 SELECTION, PROJECTION 还是 JOIN 等
  2. 访问方式 (Access Method):定义了文件结构,是索引式 (Indexed),哈希 (Hashed) 等

举一个具体的例子:

在索引文件上进行主键选择 (Primary Key Selection on Indexed Table)

我们最感兴趣的莫过于查询方法的代价 (Cost)。同时,DBMS 中的 Relational Operators 由一个 SQL Query 触发,比如:

SELECT ... FROM R WHERE C

即为在 Table R 的数据文件 (Files) 中找到满足条件 C 的 Tuples

Cost Model

正如前文所述,我们需要分析不同查询方法 (Query Method) 的代价 (Cost)。这里的代价 (Cost) 可以主要由两部分组成:

  • Time cost:执行该查询方法所用的总时间
  • Page cost:执行该查询方法过程中读写的总 page 数量

对于我们的代价模型 (Cost Model),我们有一些基本的假设:

  1. RAM (Memory) 比较小,但是读写速度很快,一次读写一个 Byte
  2. Disk 比较大,但是读写较慢,一次读写一个 page

在代价分析中,因为 Time cost 会被多种因素影响,比如硬件因素,机器的加载速度等,所以我们更多关注 Page cost。因为识别查询方法 (Query Method) 所施加的工作量很方便,但是它也会受到 Buffering 的影响,比如有多少个 Buffer,选择了什么 Replacement Stragegy。因此,我们在分析 Page cost 的时候,会不考虑 Buffering 带来的影响,简单地认为每次读/写/更新操作都会直接产生 Disk I/O。 需要注意的是,我们的模型只是进行 “粗略” 的估计

Scanning

这一节我们主要来看所有关系逻辑操作中最简单的一种:Scanning。首先看一个最简单的例子:

SELECT * FROM Rel;

这里 Table Rel 的文件结构 (File Structure) 为:

在这里插入图片描述
上述查询会扫描所有 page 中的所有 tuple,即:

for each page P in file of relation Rel {
	for each tuple T in page P {
		add tuple T to result set
	}
}

因此这里的 Page cost = Number of all pages = b。下面考虑 Data File 拥有 Overflow Page 的情况:

在这里插入图片描述
此时的 Page cost = b + bOV (bOV 表示 overflow page 的总数)

目前我们所考虑的都是完全扫描,现在我们来看另一个例子:

SELECT * FROM Employee WHERE id=762288;

这是一个条件查询,我们保证该查询只有一个答案,可能在任意 page 中。

在这里插入图片描述
此时,就不需要在进行完全扫描,只要找到匹配的结果,就可以立即终止扫描。即:

for each page P in relation Employee {
	for each tuple t in page P {
		if (t.id == 762288) return t
}  }

这时考虑 Page cost 就会有三种情况:

  • 最坏的情况 (Worst Case):完全扫描/找不到答案,Cost=b
  • 最好的情况 (Best Case):一发几种,Cost=1
  • 平均情况 (Averagr Case):平均扫描半数的 page,Cost=b/2

上述的扫描,我们一般都需要使用迭代器 (Iterator) 来实现,一般 scan 就是一个迭代器的实例:

Scan s = start_scan(Relation r, ...)

确定对 Table r 进行扫描,后面的 … 表示可以添加一些条件,比如 WHERE 语句

这里还需要另一个函数来结束扫描:

Tuple next_tuple(Scan s)

该函数在访问完最后一个 Tuple 后,会立刻返回 Tuple。如果没有更多的 Tuple 则返回 NULL

下面是一个具体的例子,实现 SELECT name FROM Employee;

 DB db = openDatabase("myDB"); 
 Relation r = openRelation(db,"Employee",READ); 
 Scan s = start_scan(r); 
 Tuple t; // current tuple 
 while ((t = next_tuple(s)) != NULL) { 	
 	char *name =
 	getStrField(t,2); 	
 	printf("%s\n", name); 
 }

Scan 实际上是一个指向 Struc Scandata 的指针,这个 Struc 会保有一些进行扫描的关键信息

typedef ScanData *Scan;
typedef struct {
 	Relation rel;
 	Page *curPage; // Page buffer
 	int curPID; // current pid 	
 	int curTID; // current tid
}	ScanData;

Copying

考虑如下的一个语句:

CREATE TABLE T AS (SELECT * FROM S);

该语句实际上将一个 Table 中的数据全部复制给一个新的 Table。所以它的实际操作流程为:

make empty relation T
s = start scan of S
while (t = next_tuple(s)) {
	insert tuple t into relation T
}

该操作最后得到的 Table T 可能会小于原始的 Table S。其原因在于,原本的 Table S 的 Pages 中,可能会有一些 Tuple 被删除了,它们空出了一部分空间,而在复制的过程中,系统实际上是不断向新的 Table T 中 append Tuple,因此会变得比较紧密 (Compact)。

在这里插入图片描述

Sorting

现在来看另一种非常常见的逻辑关系操作 - 排序 (Sorting)。在使用 SQL 查询时,可以使用 ORDER BY 来显式地使用查询操作。

SELECT * FROM student ORDER BY name;

但是,某些操作中也会隐式地使用排序,比如:

  • 消除重复的元组以进行 projection
  • 对文件排序以提高 SELECT 效率
  • 实现各种风格的 JOIN
  • 使用 GROUP BY 来进行聚合

快速排序等排序方法是针对内存中数据 (In-memory data) 设计的。对于磁盘上的大数据,需要外部排序 (External sort),例如合并排序 (Merge sort)。 下面给出 2 路归并算法的示意图:

在这里插入图片描述
首先在每个 page 中进行内部排序,之后再两两归并。在这个过程中,我们于内存 (Memory) 中至少需要有 3 个 buffer:

在这里插入图片描述
我们假设在内存中,两个 buffer 的合并操作的代价约为 0。上述的这个方法中,很显然我们需要不断进行比较 (Comparision)。因此,我们可以想象一个比较函数 tupCompare(r1, r2, f)

int tupCompare(r1,r2,f)
{
	if (r1.f < r2.f) return -1;
	if (r1.f > r2.f) return 1;
	return 0;
}

在这个函数中,我们指定需要进行对比的两个 tuples,以及按照哪个属性 (attribute) 来进行对比。但是在实际情况中,我们往往需要基于多个属性 (attributes) 来进行对比。

SELECT * FROM students ORDER BY age desc, year_enrolled;

这时函数应当变为:

int tupCompare(r1,r2,criteria)
{
	foreach (f,ord) in criteria {
		if (ord == ASC) {
			if (r1.f < r2.f) return -1;
			if (r1.f > r2.f) return 1;
		}
		else {
			if (r1.f > r2.f) return -1;
			if (r1.f < r2.f) return 1;}
		}
	return 0;
}

Cost Analysis

对于一个包含 b pages 的 file:

  • 需要 ceiling(log2b) 轮次去进行排序
  • 每一轮需要读取 (read) b pages,以及写 (write) b pages

所以,total cost = 2b * ceiling(log2b)

下面来看 n 路归并,在 n 路归并的一开始,需要进行一轮初始化操作:

在这里插入图片描述
在这一轮操作中,使用全部的 B 个 Buffer 来读取文件中的 pages,之后对读取的这些 pages 进行内排序,之后将排序后的结果整体打包,存入文件,该文件中,每一个 Run 的尺寸即为 B pages。这一环节就被称为初始 Runs 的生成。输出文件中会有多个这样的 Runs。

接下来,进行递归的合并操作:

在这里插入图片描述
此时不再使用全部的 B 个 Buffer,而是使用 B- 1 个 Buffer,这样将 N - 1 个 Runs 进行合并,得到一个新的排好序的 Run,也就是 N-1路归并 (N-1 - way Merge),每个新的 Run 的尺寸即为 N*(N - 1) Pages。之后不断重复此操作,最终得到一个排好序的文件。

N-路归并的总代价为:

2*b * (1 + ceiling(logNb0))

这里的 1 就是初始轮次,b0 = ceiling(b/B), N = B-1

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值