SQL 执行是由数据库解析器转化为一个由多个 executor 组成的 Query Plan 来完成的,本实验选择了火山模型来完成 query execution,这一次的 project 就是实现各种 exeutor,从而可以通过组装这些 executor 来完成数据库的查询、插入等操作。
火山模型如下图所示:
SQL 被转换为一系列 executor 的执行,首先会对每个 executor 调用 Init()
方法来进行初始化,之后上层的 executor 通过调用下层的 executor 的 Next()
方法来获取一个 tuple,每一个 executor 都是如此,就类似一个火山一样,数据从底部逐渐向上喷发。
Task 1:
cd build
make catalog_test
./test/catalog_test
Task 2~3:
cd build
make executor_test
./test/executor_test
这是一个热身任务,较为简单。
DB 内部通过维护 catalog 来维护数据库的元数据(比如 table 的创建与获取等),这个 task 需要实现 SimpleCatalog
的 CreateTable()
和 GetTable()
方法。
数据库的 table 主要包含 name 和 oid 两个属性,catalog 通过 table_
属性维护 table 的 name 与 oid 的映射关系,其 GetTable()
方法需要同时支持对这两个的查询:
GetTable(const std::string &table_name)
:通过 name 来获取 tableGetTable(table_oid_t table_oid)
:通过 oid 来获取 table一个 table 的元数据如下:
using table_oid_t = uint32_t;
using column_oid_t = uint32_t;
struct TableMetadata {
TableMetadata(Schema schema, std::string name, std::unique_ptr<TableHeap> &&table, table_oid_t oid)
: schema_(std::move(schema)), name_(std::move(name)), table_(std::move(table)), oid_(oid) {}
Schema schema_;
std::string name_;
std::unique_ptr<TableHeap> table_;
table_oid_t oid_;
};
Catalog 的关键属性字段有如下:
tables_
:记录 table oid -> table metadata 的映射names_
:记录 table name -> table oid 的映射next_table_oid
:记录创建下一个 table 时可以分配的 oidcatalog 创建 table 的实现:
/**
* Create a new table and return its metadata.
* @param txn the transaction in which the table is being created
* @param table_name the name of the new table
* @param schema the schema of the new table
* @return a pointer to the metadata of the new table
*/
auto CreateTable(Transaction *txn, const std::string &table_name, const Schema &schema) -> TableMetadata * {
BUSTUB_ASSERT(names_.count(table_name) == 0, "Table names should be unique!");
table_oid_t table_oid = next_table_oid_++; // 这行代码很妙,在自增了 atomic 变量的同时将旧值返回给了 oid
auto *tableHeap = new TableHeap(bpm_, lock_manager_, log_manager_, txn);
auto *tableMetadata = new TableMetadata(schema, table_name, std::unique_ptr<TableHeap>(tableHeap), table_oid);
tables_.insert({table_oid, std::unique_ptr<TableMetadata>(tableMetadata)});
names_.insert({table_name, table_oid});
return tableMetadata;
}
catalog 查询 table 的实现:
/** @return table metadata by name */
auto GetTable(const std::string &table_name) -> TableMetadata * {
const table_oid_t table_oid = names_.at(table_name);
const auto& table = tables_.at(table_oid);
return table.get();
}
/** @return table metadata by oid */
auto GetTable(table_oid_t table_oid) -> TableMetadata * {
const auto &table = tables_.at(table_oid);
return table.get();
}
这个 task 就是要实现各个 executor:
seq_scan_executor
:顺序扫描一个 table,每次 Next()
生成一个 tuple。insert_executor
:在一个 table 中插入 tuples。所要插入的 tuple 可能来源于指定的 tuple(称为 raw insert),也可能来源于调用 child executor 的 Next()
所得到的 tuple(称为 not-raw insert)。hash_join_executor
:使用 hash join 的算法将两个 table 进行 join,每次 Next()
生成一个 join 后的 tuple。aggregation_executor
:使用 aggregation 的相关计算,在本 task 中具体指的是带有 having 子句的 COUNT, SUM, MIN, MAX 这四个计算。这些 executor 在每次生成一个 SQL 对应的 query plan 时被实例化,并在构造函数中传入上下文信息,在执行时保证在第一次调用 Next()
前会先被调用 Init()
来初始化。
构造函数中主要传入如下几个上下文信息:
ExecutorContext *exec_ctx
:包含 buffer pool manager、transaction、catalog 等关于本次执行的全局上下文信息。plan
:一个 AbstractPlanNode 的子类的对象,它指示了这个 executor 本次所需要执行的任务的相关信息,比如这个 executor 需要输出 tuple 的 schema、用于过滤的判断谓词 predicate、aggregation 计算的具体类型(比如是 SUM 还是 MIN)等等。每种 executor 传入的 plan 也不同。child executor
:一个 executor 可能需要调用 child executor 的 Next()
方法来获得 tuple,从而完成自己的 Next()
。比如 insert executor 需要从 child executor 来获取需要插入的 tuple,hash join executor 需要从 left child executor 和 right child executor 来获取需要进行 join 的左右两张表的 tuple。executor 每次调用 Next()
后需要将相关状态记录到 executor 对象的属性中,这样下次 Next()
知道要继续做什么。
一个 Tuple 类的实例就是一个 tuple,它包含两个关键属性:
char *data_
:字节数组,表示这个 tuple 所拥有的数据uint32_t size_
:表示 data_
的长度它有一个关键的构造函数:
// constructor for creating a new tuple based on input value
Tuple(std::vector<Value> values, const Schema *schema);
values 是这个 tuple 的各个 column 的值,schema 表示如何解读这个 values,构造函数会将其序列化为字节数组。
该 executor 顺序扫描一个 table,将满足 predicate 条件的所有 tuple 返回出去。
内部维护一个用于遍历 table 的 iterator,每次调用 Next()
时便会将 iterator 指向第一个满足 predicate 的 tuple 返回回去。
SeqScanExecutor::SeqScanExecutor(ExecutorContext *exec_ctx, const SeqScanPlanNode *plan)
: AbstractExecutor(exec_ctx),
plan_(plan),
table_metadata_(exec_ctx->GetCatalog()->GetTable(plan->GetTableOid())),
table_iterator_(table_metadata_->table_->Begin(exec_ctx->GetTransaction())) {}
void SeqScanExecutor::Init() {}
auto SeqScanExecutor::Next(Tuple *tuple) -> bool {
const auto *const predicate = plan_->GetPredicate();
while (table_iterator_ != table_metadata_->table_->End()) {
*tuple = *table_iterator_;
++table_iterator_;
if (predicate == nullptr || predicate->Evaluate(tuple, &table_metadata_->schema_).GetAs<bool>()) {
return true;
}
}
return false;
}
用于向 table 中插入 tuples,调用 Next()
时插入所有的 tuples。根据所要插入的 tuple 的来源不同,分为两种情况:
plan_
中指定的 tupleNext()
来获得InsertExecutor::InsertExecutor(ExecutorContext *exec_ctx, const InsertPlanNode *plan,
std::unique_ptr<AbstractExecutor> &&child_executor)
: AbstractExecutor(exec_ctx),
plan_(plan),
child_executor_(std::move(child_executor)),
table_metadata_(exec_ctx->GetCatalog()->GetTable(plan->TableOid())) {}
auto InsertExecutor::GetOutputSchema() -> const Schema * { return plan_->OutputSchema(); }
void InsertExecutor::Init() {}
auto InsertExecutor::Next([[maybe_unused]] Tuple *tuple) -> bool {
return plan_->IsRawInsert()? RawInsert(): NotRawInsert();
}
auto InsertExecutor::RawInsert() -> bool {
RID rid;
const auto& rows = plan_->RawValues();
for (const auto& rowValues : rows) {
Tuple tuple(rowValues, &table_metadata_->schema_);
if (!table_metadata_->table_->InsertTuple(tuple, &rid, exec_ctx_->GetTransaction())) {
return false;
}
}
return true;
}
auto InsertExecutor::NotRawInsert() -> bool {
Tuple tuple;
RID rid;
child_executor_->Init();
while (child_executor_->Next(&tuple)) {
if (!table_metadata_->table_->InsertTuple(tuple, &rid, exec_ctx_->GetTransaction())) {
return false;
}
}
return true;
}
这个 executor 使用 hash join 算法将两个 table 进行 join,这个算法可以分为两步:
在这个 task 中,hash table 使用了一个构建于内存中的 SimpleHashTable,build 阶段在 Init()
中完成,每次 Next()
返回一个 probe 阶段生成的 tuple。
HashJoinExecutor::HashJoinExecutor(ExecutorContext *exec_ctx, const HashJoinPlanNode *plan,
std::unique_ptr<AbstractExecutor> &&left, std::unique_ptr<AbstractExecutor> &&right)
: AbstractExecutor(exec_ctx),
plan_(plan),
jht_("simple-ht", exec_ctx->GetBufferPoolManager(), jht_comp_, jht_num_buckets_, jht_hash_fn_),
left_(std::move(left)),
right_(std::move(right)),
collisions_(nullptr),
collision_index_(0) {}
void HashJoinExecutor::Init() {
left_->Init();
right_->Init();
// HashJoin build 阶段
Tuple tuple;
auto * const txn = exec_ctx_->GetTransaction();
while (left_->Next(&tuple)) {
const auto hash_value = HashValues(&tuple, left_->GetOutputSchema(), plan_->GetLeftKeys());
jht_.Insert(txn, hash_value, tuple);
}
}
auto HashJoinExecutor::Next(Tuple *tuple) -> bool {
// HashJoin probe 的阶段
while (NextCursor()) {
// 初始化相关数据
const auto *const predicate = plan_->Predicate(); // 用于判断是否能够 join 的 predicate
auto *left_tuple = &collisions_->at(collision_index_); // left tuple
auto *right_tuple = &right_tuple_; // right tuple
const auto *const output_schema = this->GetOutputSchema(); // join 之后的 output schema
const auto col_count = output_schema->GetColumnCount(); // output 的 column 个数
// 判断 left-tuple 和 right-tuple 是否能够 join
bool jointable = true;
if (predicate != nullptr) {
jointable = predicate->EvaluateJoin(left_tuple, left_->GetOutputSchema(), right_tuple, right_->GetOutputSchema()).GetAs<bool>();
}
if (jointable) {
// 将两个 tuple 进行 join
std::vector<Value> values;
for (size_t col_index = 0; col_index < col_count; col_index++) {
const auto& expr = output_schema->GetColumn(col_index).GetExpr();
const auto& value = expr->EvaluateJoin(left_tuple, left_->GetOutputSchema(), right_tuple, right_->GetOutputSchema());
values.push_back(value);
}
*tuple = Tuple(values, output_schema);
return true;
}
}
return false;
}
/** 指向下一个待匹配的 left tuple 和 right tuple */
auto HashJoinExecutor::NextCursor() -> bool {
collision_index_++;
while (collisions_ == nullptr || collision_index_ >= collisions_->size()) {
bool has_more = right_->Next(&right_tuple_);
if (!has_more) {
return false;
}
auto r_hash_value = HashValues(&right_tuple_, right_->GetOutputSchema(), plan_->GetRightKeys());
collisions_ = std::make_unique<std::vector<Tuple>>();
jht_.GetValue(exec_ctx_->GetTransaction(), r_hash_value, collisions_.get());
collision_index_ = 0;
}
return true;
}
这个 executor 实现带有 having 条件的 COUNT, SUM, MIN, MAX 这四种聚合计算。
实现 aggregation 也分成两步:
本 project 已经帮我们实现了一个 SimpleAggregationHashTable,它有一个已实现好的 InsertCombine()
方法,我们只需要遍历所有 tuples,然后分别调用 InsertCombine
,就会帮我们完成分区和每个分区的 aggregation value 的计算。
所以我们可以在 executor 的 Init()
阶段遍历所有 tuples,并调用 InsertCombine()
计算出所有分区的 aggregation value,在 Next()
遍历每一个分区的聚合结果,判断一下是否满足 having 条件,将每个满足条件的分区构造为一个 tuple 返回出去。
AggregationExecutor::AggregationExecutor(ExecutorContext *exec_ctx, const AggregationPlanNode *plan,
std::unique_ptr<AbstractExecutor> &&child)
: AbstractExecutor(exec_ctx),
plan_(plan),
child_(std::move(child)),
aht_(plan_->GetAggregates(), plan_->GetAggregateTypes()),
aht_iterator_(aht_.End()) {}
auto AggregationExecutor::GetChildExecutor() const -> const AbstractExecutor * { return child_.get(); }
auto AggregationExecutor::GetOutputSchema() -> const Schema * { return plan_->OutputSchema(); }
void AggregationExecutor::Init() {
child_->Init();
// 构建 hash table
Tuple tuple;
while (child_->Next(&tuple)) {
const auto agg_key = MakeKey(&tuple);
const auto agg_value = MakeVal(&tuple);
aht_.InsertCombine(agg_key, agg_value); // 相当于 ReHash 阶段的存储 (GroupKey -> RunningVal)
}
aht_iterator_ = aht_.Begin();
}
auto AggregationExecutor::Next(Tuple *tuple) -> bool {
const auto *const having_predicate = plan_->GetHaving();
while (aht_iterator_ != aht_.End()) {
const auto &agg_key = aht_iterator_.Key();
const auto &agg_value = aht_iterator_.Val();
++aht_iterator_;
// 判断是否满足 having 条件
bool isSatified = true;
if (having_predicate != nullptr) {
isSatified = having_predicate->EvaluateAggregate(agg_key.group_bys_, agg_value.aggregates_).GetAs<bool>();
}
// 如果满足 having 条件,则将 agg_key 和 agg_value 构建为 output_schema 的形式,并返回
if (isSatified) {
std::vector<Value> values;
const auto *const output_schema = GetOutputSchema();
for (size_t i = 0; i < output_schema->GetColumnCount(); i++) {
const auto * const expr = output_schema->GetColumn(i).GetExpr();
values.push_back(expr->EvaluateAggregate(agg_key.group_bys_, agg_value.aggregates_));
}
*tuple = Tuple(values, GetOutputSchema());
return true;
}
}
return false;
}
之前我们实现 hash join executor 时,使用的 hash table 是一个构建于内存的 SimpleHashTable
,当内存无法存放一个 left table 的所有 tuples 时,这种 hash table 便会无法完成运算。本 task 目标就是实现将 hash join executor 中的 SimpleHashTable
更换为我们在 project-2 中实现的构建于磁盘的 LinearProbeHashTable
。
由于 Tuple
类中的 data_
是一个字节数组,存储有这个 tuple 的实际数据,占用空间较大,将它作为 hash table 的 value 时会惹上很多麻烦,所有本 project 提出可以先实现一个 TmpTuple 和 TmpTuplePage:
TmpTuplePage
:它是 Page
类的子类,由 buffer pool manager 管理,这种 page 专门用于存储 Tuple
对象,其中包含这个 tuple 的实际数据。TmpTuple
:每个它的实例对应一个真正的 tuple,但它不存储 tuple 的实际数据,而是存储了这个 tuple 在哪个 TmpTuplePage 和它在这个 page 上的 offset,这样在构建 hash table 时,可以使用 TmpTuple
对象来作为 value。本 project 帮我们设计了 TmpTuplePage
的空间布局:
/**
* TmpTuplePage format:
*
* Sizes are in bytes.
* | PageId (4) | LSN (4) | FreeSpace (4) | (free space) | TupleSize2 (4) | TupleData2 | TupleSize1 (4) | TupleData1 |
*
* We choose this format because DeserializeExpression expects to read Size followed by Data.
*/
TmpTuple 如下:
class TmpTuple {
public:
TmpTuple() = default;
TmpTuple(page_id_t page_id, size_t offset) : page_id_(page_id), offset_(offset) {}
inline bool operator==(const TmpTuple &rhs) const { return page_id_ == rhs.page_id_ && offset_ == rhs.offset_; }
page_id_t GetPageId() const { return page_id_; }
size_t GetOffset() const { return offset_; }
void SetPageId(page_id_t page_id) { page_id_ = page_id; }
void SetOffset(size_t offset) { offset_ = offset; }
private:
page_id_t page_id_;
size_t offset_;
};
TmpTuplePage 如下:
class TmpTuplePage : public Page {
public:
void Init(page_id_t page_id, uint32_t page_size) {
// Set the page ID.
memcpy(GetData(), &page_id, sizeof(page_id_t));
// Set FreeSpace
// 因为 free-space-pointer 一开始应该是指向 page 的最末尾处,所以 free-space-pointer 的值应当就是 PAGE_SIZE
SetFreeSpacePointer(page_size);
}
auto GetTablePageId() -> page_id_t { return *reinterpret_cast<page_id_t *>(GetData()); }
auto Insert(const Tuple &tuple, TmpTuple *out) -> bool {
const auto tuple_size = tuple.GetLength();
// determine whether there is enough space to insert tuple
if (GetFreeSpaceRemaining() < tuple_size + SIZE_TUPLE_SIZE) {
return false;
}
// insert tuple and its size
SetFreeSpacePointer(GetFreeSpacePointer() - tuple_size);
memcpy(GetData() + GetFreeSpacePointer(), tuple.GetData(), tuple_size);
SetFreeSpacePointer(GetFreeSpacePointer() - SIZE_TUPLE_SIZE);
memcpy(GetData() + GetFreeSpacePointer(), &tuple_size, SIZE_TUPLE_SIZE);
out->SetPageId(GetPageId());
out->SetOffset(GetFreeSpacePointer());
return true;
}
auto Get(uint32_t offset) -> char * {
return GetData() + offset;
}
private:
static_assert(sizeof(page_id_t) == 4);
static constexpr size_t SIZE_TABLE_PAGE_HEADER = 12;
static constexpr size_t SIZE_TUPLE_SIZE = 4; // TupleSize 字段的 size
static constexpr size_t OFFSET_FREE_SPACE = 8;
void SetFreeSpacePointer(uint32_t free_space_pointer) {
memcpy(GetData() + OFFSET_FREE_SPACE, &free_space_pointer, sizeof(free_space_pointer));
}
auto GetFreeSpacePointer() -> uint32_t { return *reinterpret_cast<uint32_t *>(GetData() + OFFSET_FREE_SPACE); }
auto GetFreeSpaceRemaining() -> uint32_t { return GetFreeSpacePointer() - SIZE_TABLE_PAGE_HEADER; }
};
完成了 TmpTuplePage
和 TmpTuple
,我们就可以利用它们和 LinearProbeHashTable
共同实现 hash join executor。
我们将 tuple 在 key columns 上的 values 计算的哈希值作为 key,将这个 tuple 对应的 tmp_tuple 作为 value,来构建 hash table。
由于 LinearProbeHashTable
是一个 C++ template,因此我们需要先在 hash_table_block_page.cpp 和 linear_probe_hash_table.cpp 文件中添加如下代码做模板特例化:
// 在 hash_table_block_page.cpp 末尾
template class HashTableBlockPage<hash_t, TmpTuple, HashComparator>;
// 在 linear_probe_hash_table.cpp 末尾
template class LinearProbeHashTable<hash_t, TmpTuple, HashComparator>;
在 hash_join_executor.cpp 加上:
// 在 hash_join_executor.cpp 中
template class LinearProbeHashTable<hash_t, TmpTuple, HashComparator>;
template class HashTableBlockPage<hash_t, TmpTuple, HashComparator>;
在 HashJoinExecutor 中声明的 hash table 的类型是 HT
,之前 using HT = SimpleHashTable
,现在需要将其更换为:
using HashJoinKeyType = hash_t;
using HashJoinValType = TmpTuple;
using HT = LinearProbeHashTable<HashJoinKeyType, HashJoinValType, HashComparator>;
最后是 HashJoinExecutor 的实现代码:
template class LinearProbeHashTable<hash_t, TmpTuple, HashComparator>;
template class HashTableBlockPage<hash_t, TmpTuple, HashComparator>;
HashJoinExecutor::HashJoinExecutor(ExecutorContext *exec_ctx, const HashJoinPlanNode *plan,
std::unique_ptr<AbstractExecutor> &&left, std::unique_ptr<AbstractExecutor> &&right)
: AbstractExecutor(exec_ctx),
plan_(plan),
jht_("linear-probe-ht", exec_ctx->GetBufferPoolManager(), jht_comp_, jht_num_buckets_, jht_hash_fn_),
left_(std::move(left)),
right_(std::move(right)),
collisions_(nullptr),
collision_index_(0) {}
void HashJoinExecutor::Init() {
left_->Init();
right_->Init();
// HashJoin build 阶段
Tuple tuple;
auto *const txn = exec_ctx_->GetTransaction();
auto *const bpm = exec_ctx_->GetBufferPoolManager();
page_id_t tmp_page_id;
TmpTuple tmp_tuple;
auto *tmp_tuple_page = reinterpret_cast<TmpTuplePage *>(bpm->NewPage(&tmp_page_id));
tmp_tuple_page->Init(tmp_page_id, PAGE_SIZE);
while (left_->Next(&tuple)) {
const auto hash_value = HashValues(&tuple, left_->GetOutputSchema(), plan_->GetLeftKeys());
// 如果 tmp_page 满了的话,新建一个 page 再 insert
if (!tmp_tuple_page->Insert(tuple, &tmp_tuple)) {
bpm->UnpinPage(tmp_page_id, true);
tmp_tuple_page = reinterpret_cast<TmpTuplePage *>(bpm->NewPage(&tmp_page_id));
tmp_tuple_page->Init(tmp_page_id, PAGE_SIZE);
tmp_tuple_page->Insert(tuple, &tmp_tuple);
}
// 将 tmp_tuple 插入到 hash table 中
// - tmp_tuple 中存的是实际 tuple 的位置
// - 实际的 tuple 放在了 tmp_tuple_page 中
jht_.Insert(txn, hash_value, tmp_tuple);
}
bpm->UnpinPage(tmp_page_id, true);
}
auto HashJoinExecutor::Next(Tuple *tuple) -> bool {
auto* const bpm = exec_ctx_->GetBufferPoolManager();
// HashJoin probe 的阶段
while (NextCursor()) {
// 初始化相关数据
const auto *const predicate = plan_->Predicate(); // 用于判断是否能够 join 的 predicate
// left tuple
const auto *const left_tmp_tuple = &collisions_->at(collision_index_);
auto *const left_tuple_page = reinterpret_cast<TmpTuplePage *>(bpm->FetchPage(left_tmp_tuple->GetPageId()));
Tuple left_tuple;
left_tuple.DeserializeFrom(left_tuple_page->Get(left_tmp_tuple->GetOffset()));
bpm->UnpinPage(left_tmp_tuple->GetPageId(), false);
const auto *const right_tuple = &right_tuple_; // right tuple
const auto *const output_schema = this->GetOutputSchema(); // join 之后的 output schema
const auto col_count = output_schema->GetColumnCount(); // output 的 column 个数
// 判断 left-tuple 和 right-tuple 是否能够 join
bool jointable = true;
if (predicate != nullptr) {
jointable = predicate->EvaluateJoin(&left_tuple, left_->GetOutputSchema(), right_tuple, right_->GetOutputSchema()).GetAs<bool>();
}
if (jointable) {
// 将两个 tuple 进行 join
std::vector<Value> values;
for (size_t col_index = 0; col_index < col_count; col_index++) {
const auto* expr = output_schema->GetColumn(col_index).GetExpr();
const auto value = expr->EvaluateJoin(&left_tuple, left_->GetOutputSchema(), right_tuple, right_->GetOutputSchema());
values.push_back(value);
}
*tuple = Tuple(values, output_schema);
return true;
}
}
return false;
}
/** 指向下一个待匹配的 left tuple 和 right tuple */
auto HashJoinExecutor::NextCursor() -> bool {
collision_index_++;
while (collisions_ == nullptr || collision_index_ >= collisions_->size()) {
const bool has_more = right_->Next(&right_tuple_);
if (!has_more) {
return false;
}
const auto r_hash_value = HashValues(&right_tuple_, right_->GetOutputSchema(), plan_->GetRightKeys());
collisions_ = std::make_unique<std::vector<TmpTuple>>();
jht_.GetValue(exec_ctx_->GetTransaction(), r_hash_value, collisions_.get());
collision_index_ = 0;
}
return true;
}