project3

项目整体结构:

1.查询处理层:负责将人类可读的SQL转换为机器可执行的计划。

  • Parser(解析器):将 SQL 字符串转换成抽象语法树(AST)。

  • Binder(绑定器):通过查询系统目录,将 SQL 词元(如表名、列名)映射到具体的标识符(OID)上。

  • Planner(生成器):将绑定后的 AST 转换为逻辑查询计划。

  • Optimizer(优化器)这是 Project 3 的一部分。它通过启发式规则或代价模型,将逻辑计划转换为最优的物理查询计划。

2.查询执行层:负责实际处理数据。

  • Executors(执行器)这是 Project 3 的核心任务 。你将实现诸如 Aggregation (聚合)、Scan (扫描)、Join(连接)等算子。

  • 执行模型 :BusTub 通常使用迭代模型,上层算子不断调用下层算子的 Next() 来获取元组。

3.并发控制层:负责确保多个用户同时访问时数据的一致性。

  • Transaction Manager(事务管理器)这是 Project 4 的重点 。它负责处理 BEGINCOMMITABORT 指令,并通过锁机制保护数据。

  • 它位于执行器与底层存储之间,拦截数据的读写请求以执行 ACID 原则。

4.存储层:负责数据的物理管理。

  • Index(索引)Project 2 的内容 。你实现的 Extendible Hash Table 就位于此处,用于加速数据检索。

  • Table Heap(表堆):存储实际元组的物理结构。

  • Buffer Pool Manager(缓冲池管理器)Project 1 的内容。它作为内存与磁盘之间的缓冲层,决定哪些数据页留在内存中。

  • Disk Manager(磁盘管理器):最底层,直接与操作系统文件系统交互。

通过EXPLAIN我们可以看到planner生成的类似的信息:

复制代码
=== PLANNER ===
Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)

但此时并没有生成executor,只是生成了一棵PlanNode树。要想将其转换为Executor,还需要通过executor_factory中的CreateExecutor函数递归的创建Executor。

通过观察CreateExecutor函数,我们发现在一次查询中,我们始终使用同一exec_ctx,而每个执行器都独占一个PlanNode。这也是为什么执行器内部只保存PlanNode,而不保存exec_ctx。

注:仓库已经提供了projection,filter,values执行器的实现以供借鉴。


AbstractExpression:抽象含义为一个表达式,核心在于Evaluate方法。Evaluate方法返回一个Value类型。Tuple可以由std::vector<Tuple>和schema构造。

ColumnValueExpression:从元组中提取某一列的值。

ConstantValueExpression:代表一个常量,无论给它什么元组,Evaluate永远返回内部存的固定的Value。

ComparisonExpression:它有两个子表达式,先评估左右孩子的值,然后根据比较符返回一格布尔类型的Value。

ArithmeticExpression:处理如A+B这样的数学运算。


模式:模式规定了数据库有多少列,每一列叫什么名字,每一列是什么数据类型,以及这些列在内存中如何排列。在project3中,会有两种模式。

1.Table Schema:这是表在磁盘上的原始结构,可以通过table_info->schema_得到。

2.Output Schema:算子的输出模式,这是算子返回给上层的结构,可以通过plan_->OutputSchema()得到。


关于索引,一张表可能有多个索引,可能对于学生表来说,既把它的id作为索引,又把它的成绩作为索引。每个索引都对应一个IndexInfo类,每个IndexInfo类又对应一个Index类。

关于我们之前的project2,Hash Index,其实它并不负责将数据真正地插入到内存中。它只是建立"一个已经存在于内存中的元组"和索引结构之间的关系,即只负责建立映射关系。而在Insert执行器中,一个tuple从产生到真正在数据库中持久化需要经过下面几个阶段。

1.执行器调用child_executor->Next(),此时child_tuple仅存在于CPU寄存器或内存的临时缓冲区中。

2.执行器调用table_heap_->InsertTuple(meta, child_tuple)。TableHeap协同bpm在磁盘中寻找一个空位,然后将child_tuple写入缓冲池中,该操作会返回一个RID。

省略


数据库算子分为两类,流水线型和物化型。

对于前者,Next一次只需要处理一个从下层获取的Next。对于后者,一次需要处理所有从下层获取的Next。

在bustub中,只有只读型算子才可以作为流水线型,而写操作算子都是物化型。

除此之外,也有一些只读型算子是物化型。

之所以写操作算子是物化型,有以下原因:

1.结果定义冲突,修改类算子要求是汇报执行结果,比如insert算子要求返回一个元组包含插入数目。为了算出这个数目,必须等待所有子算子执行完毕。

2.万圣节问题:万圣节问题是指一个写操作改变了数据的物理位置或索引值,导致同一个扫描操作在同一条SQL中再次读到了这行数据,从而引发无限循环或重复修改。

3.索引维护的原子性:在修改一行时,如果中间某个索引插入失败(比如唯一键冲突),而你已经把前面的几行给改了且"流"给了上层,这时候很难进行局部的回滚。


代码结构:

1.executor_context,一张表只有一个。

2.catalog,系统目录,我们通过与之交互得到table_info 和 index_info。

3.table_info,通过它可以得到table_heap。

4.index_info,通过它可以得到index。


提示:

1.关于项目文档中下面这样的信息,对项目其实很有帮助。

复制代码
bustub> CREATE TABLE t1(v1 INT, v2 VARCHAR(100));
Table created with id = 15
bustub> EXPLAIN (o,s) SELECT * FROM t1;
=== OPTIMIZER ===
SeqScan { table=t1 } | (t1.v1:INTEGER, t1.v2:VARCHAR)

2.对于所有执行器,都必须确保它能够终止,即return false。如果一个执行器中没有一个return false,那么该执行器永不终止。

3.尤其注意万圣节问题,注意边读边改的做法是否会有问题。

4.聚合函数的哈希表如何运作:

一.键:根据group_bys表达式数组得到足以作为分组依据的表达式,将该表达式作为键然后进行分组。

二.值:同一个分组可能有多个聚合值,这些聚合值的集合就是哈希表的值。

三.每插入一个元组,我们先得到其键,找到对应分组,然后再更新组内的聚合值集合。

注:在窗口函数的实现过程中,哈希表的键则为partition_by,如果有多个partition_by,则应该建多张哈希表。

相关推荐
嵌入式×边缘AI:打怪升级日志10 小时前
编写 Bootloader 实现烧录功能
数据库
砚边数影11 小时前
模型持久化(二):从 KingbaseES 加载模型,实现离线预测
数据库·机器学习·kingbase·模型推理·数据库平替用金仓·金仓数据库
Ama_tor12 小时前
Navicat学习01|初步应用实践
数据库·navicat
山岚的运维笔记12 小时前
SQL Server笔记 -- 第65章:迁移 第66章:表值参数
数据库·笔记·sql·microsoft·sqlserver
番茄去哪了13 小时前
苍穹外卖day05----店铺营业状态设置
java·数据库·ide·redis·git·maven·mybatis
暮色妖娆丶15 小时前
Spring 源码分析 事务管理的实现原理(下)
数据库·spring boot·spring
暮色妖娆丶15 小时前
Spring 源码分析 事务管理的实现原理(上)
数据库·spring boot·spring
LaughingZhu15 小时前
Product Hunt 每日热榜 | 2026-02-18
大数据·数据库·人工智能·经验分享·搜索引擎
是吕先森15 小时前
mysql基础
数据库·mysql·oracle