2023年秋24组实验报告
1. 整体设计
1.1 基本设计
项目使用 C++ 编写,依赖于 antlr4-runtime 和 antlr4-cpp 这两个外部库进行语法解析,使用 argparse 解析命令行参数,此外还使用 googletest 编写了 B+ 树的功能测试。
1.2 模块设计
模块设计基本上参照实验指导书,分为文件管理模块、记录管理模块、索引管理模块、 SQL 解析模块和查询处理模块。文件管理模块的,SQL 解析模块的实现在 src/frontend 目录下,其余三个模块的实现均在 src/engine 目录下。
1.3 功能实现
除了全部基本功能外,本项目还实现了具有 CI 测例的全部附加功能: - 多表 join - 聚合查询、模糊查询、分组查询、嵌套查询、查询结果排序 - 日期、完整 null 支持 - unique 约束
2. 具体设计
2.1 工具模块
该模块的代码实现在 src/utils 目录下。为了方便参数管理,我实现了一个单例模式的 Config 类用于管理全部配置,它负责解析命令行参数并提供给其他模块使用。
为了方便统一数据库的输出,我实现了一系列 tabulate 函数,能够按照当前交互模式(交互或者批处理)来输出查询结果。
此外,这部分还提供诸如 ensure_file 和 ensure_directory 的函数,方便在其他模块中检查文件和目录,提升代码健壮性。
2.2 文件管理模块
该模块的代码实现在 src/storage 目录下。这部分代码在功能上与提供的参考代码类似,但是我使用更加现代化的 C++ 实现进行了优化。
具体而言,文件管理系统实现为单例模式的 FileMapping 类,除了正常文件的打开关闭以外,还支持创建和删除临时文件(利用 mkstemp)。在销毁时,它会关闭所有文件并自动删除临时文件。
分页缓存池被实现成单例模式的 PagedBuffer 类,它通过 FileMapping 管理文件,支持按需读取和写回,支持 LRU 策略的缓存替换。在被销毁时它会自动将全部脏页写入文件中,它通过 std::shared_ptr 来确保 FileMapping 在它之后销毁。
SequentialAccessor 用于实现顺序的读写,它内部保存一个 fd 和一个 offset,能显著地方便上层实现。例如,当需要将表的元数据写入文件时,表可以将 SequentialAccessor 的引用传给属于它的 Fields, Fields 就可以直接将自己的数据写入文件。
此外,由于 B+ 树本身与数据库模块解耦,因此将其放在文件管理模块中,它提供各种查询接口供索引管理模块使用。为了确保 B+ 树实现正确,我实现了一个 googletest 测例,它对 B+ 树进行插入、删除、查找等操作,并检查其正确性,详细实现位于 test /test_btree.cpp。
2.3 记录管理模块
该模块用于实现记录存储,代码位于 src/engine/record.cpp 中。记录数据分页存储,每一页大小为 8 KB,每页开头记录该页有效记录的数量、下一个空页的页号,紧跟着是一个 FixedBitmap ,用于记录每一个 slot 是否被使用,数据采用定长类型进行存储。记录长度、每页最大记录数量、第一个空页的页号等数据与表的元数据存储在一起。 RecordManager 实现了对单张表记录读写的管理。提供了数据插入、删除、查找、修改的接口。
2.4 索引管理模块
该模块用于实现索引存储,代码位于 src/engine/index.cpp 中。 IndexMeta 包含一个B+ 树和一个引用计数,当引用计数为 0 时, B+ 树会被销毁。数据库进程结束时,主键、外键、 unique 约束等在存储时仅需标记它们取了哪些列,下次启动时,数据库仅需按照这些列找到对应的 IndexMeta 即可。
2.5 查询处理模块
查询的执行基于迭代器实现;查询中的限制检查和修改操作分别由 WhereConstraint 和SetVariable 执行。对于增删改操作,需要对各种约束情况加以考虑。以下详细介绍各个部分的实现思路。
2.5.1 迭代器
迭代器从基本功能上分为两类,一类从文件(记录和索引)中读取数据(并进行筛选),另一类在前一类的基础上进行连接、过滤、聚合等操作。而从具体优化上,可以允许迭代器一次从文件中读取多条记录(可以称为“带缓冲的分块读取”),这样在连接时可以采用分块的优化,减少磁盘 IO 次数。在实现时,迭代器往往需要记录它迭代的来源的列信息、它提供的结果的列信息、当前迭代的位置、传入的约束等信息。本项目实现了以下几种迭代器。
RecordIterator访问记录文件,分块读取IndexIterator访问索引文件,分块读取JoinIterator连接两个迭代器,这两个迭代器需要支持分块读取PermuteIterator对上一个迭代器的列进行重排AggregateIterator对上一个迭代器的结果进行聚合操作,在执行聚合时会顺便进行列重排SortIterator对上一个迭代器的结果按照某一关键词进行排序
迭代器之间构成了一个树的结构,数据先由 RecordIterator 或者 IndexIterator 进行提取,然后由 JoinIterator 连接,接着通过 PermuteIterator 或 AggregateIterator 进行列重排(或进行聚合),最后如需排序,则通过 SortIterator 。输出函数不断地从最顶部的迭代器
取出元素,使各个迭代器不断地产生、筛选新的记录,整个过程类似于流水线。
QueryPlanner 的功能是按照查询语句构建迭代器树,当 GROUP BY 所需的列不在查询的列中时,它负责临时将其加入; LIMIT 和 OFFSET 限制也由它进行处理。如果某一个查询是一个子查询,那么它上层查询的 WhereConstraint 会基于它的 QueryPlanner 执行;否则,QueryPlanner 会直接送给输出模块。
2.5.2 查询描述、执行
WhereConstraint 是一个基类,它要求所有子类实现一个 check 函数来检查某条记录是否满足要求,它的子类包括
- ColumnOpValueConstraint 表示列和值之间的比较关系
- ColumnOpColumnConstraint 表示列之间的比较关系
- ColumnNullConstraint 进行 NULL 检查
- ColumnLikeStringConstraint 进行字符串模糊查询
- ColumnOpSubqueryConstraint 列和子查询的比较关系
- ColumnInSubqueryConstraint 列和子查询的包含关系
SetVariable 类中记录了待修改的列的 offset,以及需要修改成的值,它能且仅能对一条记录进行修改,关于约束的检查由上层代码完成。
2.5.3 增删改操作
为了方便调用,增删改操作在 src/engine/scape_sql.cpp 中实现,这样前端在解析 SQL 语句时可以直接调用这些封装好的函数。另外,为了优化外键约束的检查,我在 B+ 树的实现中,允许叶节点存储一个 int32_t 类型的引用计数来表示这条记录被外键引用的次数。
对于 INSERT 操作,检查 PRIMARY,FOREIGN,UNIQUE 约束是否满足,若满足,则向记录以及每个 IndexMeta 管理的 B+ 树插入数据,并更新它引用的外键的引用计数。
对于 DELETE 操作,检查 FOREIGN 约束是否满足(即引用计数是否为 0),若满足要求,则可在索引和记录中删去。
对于 UPDATE 操作,虽然它等价于一次删除和一次插入,但主外键约束使得情况复杂一些。如果修改前后主键对应的列数值不变,那么只要检查外键约束是否满足即可(这就允许它的引用计数大于零);否则,则可以等价为一次删除和一次插入,如果无法删除或无法插入,则恢复原状并报错。
2.6 系统管理模块
系统管理模块包括 GlobalManager,DatabaseManager,TableManager 三个层级,代码在 src/engine/system.cpp 中。它们都具有元数据的存取功能,而对于 TableManager ,它还提供索引、主外键约束、 unique 约束的增删功能。
2.7 SQL 语句解析模块
该模块使用 antlr4 进行实现,代码位于 src/frontend 目录下。它首先使用 antlr4 生成
语法分析树,接着使用自定义的 Visitor 访问语法树,并在访问过程中实时地执行操作。
3. 接口设计
3.1 文件管理模块
class PagedBuffer {
// 为实现 LRU 刷新策略,使用双向链表
void access(int id);
// 根据指定的文件描述符和页号,从文件中读取一页
uint8_t *read_file_rd(PageLocator pos);
// 读取后将该页标记为脏页
uint8_t *read_file_rdwr(PageLocator pos);
// 将指定页标记为脏页,由于页的分配是连续的,可由指针判断属于哪个页
bool mark_dirty(uint8_t *ptr);
};
class SequentialAccessor {
void reset(int pagenum_);
// 读取接口
uint8_t read_byte();
template <typename T> T read();
std::string read_str();
// 写入接口
void write_byte(uint8_t byte);
template <typename T> void write(T val);
void write_str(const std::string &val);
};
3.2 记录管理模块
class RecordManager {
// 创建表时使用的构造函数
RecordManager(const std::string &datafile_name, int record_len);
// 从文件中读取元数据的构造函数
RecordManager(SequentialAccessor &accessor);
// 元数据文件写入
void serialize(SequentialAccessor &accessor);
// 其他一些模块也有这样一组构造函数和析构函数,为了简洁,未来不再赘述
// 获取指定位置的记录
uint8_t *get_record_ref(int pageid, int slotid);
// 插入记录并返回它的位置(页号和槽号)给索引使用
std::pair<int, int> insert_record(const uint8_t *ptr);
// 删除记录
void erase_record(int pagenum, int slotnum);
};
3.3 索引管理模块
struct IndexMeta {
// IndexMeta 本质上是 B+ 树的封装,便于从记录中提取键值
// 为一个表建立外键约束时,它的外键 Offset 与被引用表的主键 Offset 不同
// 因此需要通过 remap 函数重新建立偏移量
std::shared_ptr<IndexMeta>
remap(const std::vector<std::shared_ptr<Field>> &keys) const;
// 从记录中提取键值
std::vector<int> extractKeys(const KeyCollection &data);
// 插入记录
void insert_record(KeyCollection data);
// 小于等于匹配,上层可以利用它查询是否出现过某个 key
BPlusQueryResult le_match(KeyCollection data);
// 获取引用计数
uint32_t *get_refcount(uint8_t *ptr);
};
3.4 迭代器
class Iterator {
virtual bool get_next_valid() = 0;
virtual const uint8_t *get() const = 0;
};
class BlockIterator : public Iterator {
// 对迭代器缓存的结果块进行操作、查询
void block_next();
bool block_end();
bool all_end();
// 让迭代器筛选出下一块记录,返回填入的记录数量
virtual int fill_next_block() = 0;
// 查询当前块/全部记录是否访问结束
virtual void reset_all() = 0;
void reset_block();
};
class RecordIterator : public BlockIterator;
class IndexIterator : public BlockIterator;
class JoinIterator : public BlockIterator;
class PermuteIterator : public Iterator;
// Gather 迭代器将打断流水线,它只有在将上层迭代器的结果全部读取后才能得到新的结果
class GatherIterator : public Iterator;
class AggregateIterator : public GatherIterator;
class SortIterator : public GatherIterator;
class QueryPlanner {
// QueryPlanner 有大量的成员变量,调用前需要逐个填入,最后调用 generate_plan 生成迭代器
void generate_plan();
// 获取一条记录
const uint8_t *get() const;
// 进入下一条记录
bool next();
};
3.5 SQL 执行接口
namespace ScapeSQL {
// 简单数据库管理操作
void create_db(const std::string &s);
void drop_db(const std::string &s);
void show_dbs();
void use_db(const std::string &s);
void show_tables();
void show_indexes();
void create_table(const std::string &s,
std::vector<std::shared_ptr<Field>> &&fields);
void drop_table(const std::string &s);
void describe_table(const std::string &s);
// 增删改查操作
void update_set_table(
std::shared_ptr<TableManager> table,
std::vector<SetVariable> &&set_variables,
std::vector<std::shared_ptr<WhereConstraint>> &&where_constraints);
void delete_from_table(
std::shared_ptr<TableManager> table,
std::vector<std::shared_ptr<WhereConstraint>> &&where_constraints);
void insert_from_file(const std::string &file_path, const std::string &table_name);
// 增加、删除约束
void add_pk(const std::string &table_name, std::shared_ptr<PrimaryKey> key);
void drop_pk(const std::string &table_name, const std::string &pk_name);
void add_fk(const std::string &table_name, std::shared_ptr<ForeignKey> key);
void drop_fk(const std::string &table_name, const std::string &fk_name);
void add_index(const std::string &table_name, std::shared_ptr<ExplicitIndexKey> key);
void drop_index(const std::string &table_name, const std::string &index_name);
void add_unique(const std::string &table_name, std::shared_ptr<UniqueKey> key);
} // namespace ScapeSQL
4. 实验结果
【数据删除】
5. 参考文献
- 课程组提供的实验指导书
- Antlr4 教程(https://tomassetti.me/antlr-mega-tutorial/)
- C++ 正则表达式教程(https://en.cppreference.com/w/cpp/regex)