记录管理
- 一些概念
- 记录:数据库表中的一个元组
- 记录文件:存储表的记录的文件。
记录文件顺序存放一张表的每一条记录,并且按照数据页的形式进行划分。
RmFileHandle
类:对⼀个记录⽂件进⾏管理
RmFileHdr
类:文件头,存储在文件的第一个数据页中(也就是说第一个数据页中实际的数据内容就是文件头)
page_no范围为[0,file_hdr.num_pages),page_no从0开始增加,其中第0⻚存file_hdr,从第1⻚开始存放真正的记录数据
RmPageHandle
类:管理文件中的一个数据页(相当于是一个磁盘上物理存在的数据页对应的抽象)- page_lsn_⽤于故障恢复模块,将在故障恢复模块中进⾏详细介绍。
- page_hdr记录了两个信息,⼀个是num_records,记录当前数据⻚中已经分配的record个数,同时记录了next_free_page_no,记录了如果当前数据⻚写满之后,下⼀个还有空闲空间的数据⻚的page_no。
- bitmap记录了当前数据⻚记录的分配情况。
- slots则是真正的记录数据存放空间。
每⼀个数据⻚的开始部分并不直接存放记录数据,⽽是按照以下顺序进⾏存放:| page_lsn_ | page_hdr | bitmap | slots |
RmRecord
类:管理数据页中的一个记录(结构很简单,不做解释)
Rid
类:对记录进行唯一标识(实际上就是指明了记录存储的具体位置)
元数据存储
DbMeta
类:主要包含数据库的名称,以及数据库中所有表的元数据,这些数据(也就是数据库元数据)每执⾏⼀次DDL语句都要被写⼊到 <database.meta> ⽂件中,并且在open_db()时读取到内存中
这里的表的元数据是通过hashmap存储的
TabMeta
结构体:
需要注意的是,这个只是表的元数据,而前面的记录文件可以通过表名称查询得到
ColMeta
结构体:用于表示字段值(也就是表中的属性值)
IndexMeta
结构体:用于表示表上的索引
查询的处理与执行
- 算子:每个数据库操作对应⼀个算⼦(executor)
可以简单理解为一个关键字就对应了一个算子
- 查询过程:语法解析→语义分析→查询优化→查询所需资源初始化→查询执行→查询所需资源释放
- 语法分析:获取抽象语法树
- 语义分析:检查字段的存在性,并将抽象语法树改写为查询语句
- 查询执行:对于DDL语句和事务语句,在进⼊到portal模块的run阶段后,会和SmManager以及TransactionManager进⾏交互,进⾏DDL语句及事务语句的执⾏。
下面对每个部分进行一些记录
语法解析
正如查询的处理与执行中所提到的,语法解析的目标为根据输入的SQL语句生成抽象语法树
- 抽象语法树结构
对SQL语句:
其对应的抽象语法树结构为:
这里其实就没必要去逐一解释节点的含义了,直接结合代码和图来看就能看明白了
语义分析
语义分析相关的部分主要是在src/analyze/目录下。
do_analyze
函数:do_analyze
函数主要是根据cpp的动态类型转换来判断当前接收到的抽象语法树表示的是SelectStmt
、UpdateStmt
、DeleteStmt
还是InsertStmt
,然后根据不同的语句类型进行不同的语义检查。他的作用如下:- 进行语义检查
- 对重构查询语句query的成员进行赋值。query类的定义如下:
事务控制语句
由于客户端发来的SQL语句被视为一个事务,所以如果不先写这一题的话是没办法直接使用客户端进行调试的。所以这里先写事务控制语句中的begin。这里先不考虑并发以及锁管理的问题,只是单纯的
begin
TransactionManager
类:
其中全局事务表是一个cpp库自带的hashmap。
Transaction
类:
commit
这里还需要实现事务的commit函数。因为一个用户的所有的事务应该是串行执行的。如果一个事务完成之后状态没有被修改为COMMITTED,就不会创建下一个事务来执行,就会导致一些框架里面的断言无法通过。从下面的函数中可以看出当事务执行结束之后一定要将事务的状态修改为COMMITTED:
在commit操作中,需要将所有的写操作写入文件(也就是将所有的写操作处理掉)。写操作本质上就是对记录文件进行更改。
WriteRecord
类:表示每一个要写入的记录
从这里可以发现实际上写操作就是记录了要在那一章表的哪个位置写入数据
可以发现,写操作只针对记录数据,与元数据无关。所以对于一个纯修改元数据的事务,该写操作队列应该为空。
Q
需要完成的函数接口中有一个类型:Context,这个类型是干嘛的?
context定义如下:
这个是在用户处理函数中定义的。
Cpp相关
析构函数
如:
这个函数会在BufferPoolManager类的生命周期结束之后释放相关资源,防止内存泄漏
const
有一个地方:
这里写成:
就会报错:
这是因为get_file_hdr()函数并没有使用const关键字声明。这主要是因为函数
fetch_page_handle
是const的,这就要求所有内部使用的函数都必须是const的map
在cpp中,map可以直接像数组一样添加元素:
vector
在cpp中,vector是可变数组,与rust中的vector相似。在vector中添加元素时需要使用push函数:
前向声明
在 C++ 中,前向声明可以告诉编译器某个类的存在,但不提供类的详细定义。如果你尝试使用一个只被前向声明的类的对象,比如调用它的方法或访问它的属性,编译器就会报这个错误。
也就是说,在一个文件中可以直接使用某一个在其他文件中的类型,但不引入该类型所在的头文件。只有当要调用方法或者访问成员变量时才会报错。
一些小点
- 所有的管理器对象都被定义在文件 rmdb/src/rmdb.cpp 中(用new找半天找不到,服了)
- page_no是虚拟页号,而id就是磁盘上的物理页号,需要通过
- 由于现在还没有实现open_db,这就导致每次服务端拉起来的时候没办法打开数据库(因为open_db是一个空函数),进而就导致SmManager中的db_.name_为空字符串
层次划分
从底层到顶层说一下各个管理器的层次划分
- DiskManager:负责直接对磁盘进行操作
- BufferPoolManager:借助DiskManager的功能将磁盘上的数据读取到缓冲区中,以便进行操作
- RmManager:对记录文件进行操作
- SmManager:系统管理器,对元数据进行操作,地位比RmManager略高(因为SmManager的成员中有RmManager)
算子
- SeqScanExecutor算子:用于执行顺序扫描。这里需要重写AbstractExecutor的cols函数,因为在执行投影算子的时候,需要先通过顺序扫描算子获取列。而如果直接调用AbstractExecutor的cols一定会导致段错误。
顺序扫描算子的成员定义如下(其没有public的成员):
这里的col实际上就是一张表(可能是经过join的?)拥有的所有的字段名
编译相关
yacc.tab.h等头文件是由bison自动生成的