CMU15445-project3-坑和收获总结
排名拉了,没精力优化了
太折磨了太痛苦了,看代码的时间比写代码的时间还多,很多天都处于完全没法动键盘的状态。
感觉这个lab更像一个读代码任务,文档只讲了需要实现的笼统需求,大部分跟下手代码相关的信息全在代码里面,需要从测试文件,Executor的构造函数等地方延申读各种头文件,理解了之后才能拿来使用且完成任务,问题是还不一定能用对。
TASK #0 - BEFOR START
!!!gradescope究极大BUG!!!
上传评分的时候,自己的代码没有任何格式错误,也会在格式检查时失败:
仔细一看会发现不是自己文件的问题,官方给出的解决办法如下:
把最新的 src/include/storage/page/tmp_tuple_page.h 一起打包进提交文件即可
开始之前
value,tuple,table,column,schema
value
就是最小单位,值
tuple
相当于一行,存多个值
table
是一张表,提供了迭代器去访问,迭代器按行(tuple)遍历
column
是列的名字(抬头)
schema
则存了一串column,表示这张表有哪些列
AbstractExecutor
SQL拆分成一棵执行树之后,其中的节点的功能承担者,也是这次实验主要要完成的部分。
构造执行树时不会构造Executor,而是用后面的AbstractPlanNode
来构造树,只有在执行这棵树的时候会初始化对应的Executor来执行。
// 每个执行器需要初始化的东西都不一样,一般是初始化指向表头的迭代器指针,或者自己定义的一些辅助循环的值
// 拿seq_scan来说,就需要在这里将迭代器指针指向表头。
void Init()
// 需要做到调用一次next就输出且只输出一组tuple的功能,输出是通过赋值*tuple参数,并return true
// 如果没有能输出的了,return false
void Next(Tuple *tuple, RID *rid)
// Schema相当于存储了表的表头名字,OutputSchema用来指出此节点需要输出哪些列(哪些表头需要考虑在内)
virtual const Schema *GetOutputSchema() = 0;
ExecutorContext
(AbstractExecutor构造函数的参数之一)
上下文信息,也就是说这次执行所用到的一些关键信息
// 这个Catalog至关重要,存储了一个数据库的全部表格信息的索引,提供了对表格的操作。
// 只有这个Catalog是我们可能会用到的,比如在seq_scan中需要利用它获取目标表
Catalog *catalog_;
AbstractPlanNode
(AbstractExecutor构造函数参数之二)
用于存储节点有关的信息,AbstractExecutor
利用用里面的信息来完成任务。
// Schema是表每列的表头名字,OutputSchema用来指出此节点需要输出哪些列
Schema *OutputSchema()
// 获取孩子节点(我们实现executor的时候用不上,在执行树的时候才用得上)
// 在一些需要从子节点获取tuple的操作用得上,比如 join
AbstractPlanNode *GetChildAt(uint32_t child_idx)
std::vector<AbstractPlanNode *> &GetChildren()
例子:SeqScanPlanNode
成员函数,AbstractExecutor
可以在这里获取TableOid
和Predicate
// 这个在ExecutionEngine里面用于判断当前节点的类型
PlanType GetType()
// Predicate:谓词,返回值全是真值的表达式,AbstractExpression就是一颗表达式树。
AbstractExpression *GetPredicate()
// 结合Catlog可以得到当前Executor需要的表格内容
table_oid_t GetTableOid()
AbstractExpression
表达式类,一颗表达式树中的节点,比如比较,聚合,或者常量,column元素。其中column表达式也作为了column类的成员之一。
不同的表达式实现的功能差距交大,这个是非常重要的一个类,每一个executor的代码都会用到。
ComparisonExpression
:用于比较,实例化后做为predicate_谓词(下面例子中的predicate就是此类的实例化),或者having(在aggregation中会用到)。
返回的是一个装载Value类中的bool值,需要用value.GetAs<bool>()
得到这个值,用于判断是否满足比较的条件(比较的细节:比如是>还是<,就不用我们关心了,交给Evaluate函数就行)
Value value = plan_->GetPredicate()->Evaluate(tuple, schema)
ColumnValueExpression
:列元素的表达值,有一个很大的作用,它的Evaluate函数能返回当前tuple中哪一个value是对应这个column的。
Value value = column.GetExpr()->Evaluate(tuple, schema);
// 或者判断当前join是左连接还是右链接,并根据传入的左右俩tuple返回连接对应的值
Value value = plan_->OutputSchema()->GetColumn(i).GetExpr()->EvaluateJoin(
&left_tuple_,
plan_->GetLeftPlan()->OutputSchema(),
&right_tuple_,
plan_->GetRightPlan()->OutputSchema());
-
ConstantValueExpression
:常数表达式,返回值永远是一个常数,没用过。 -
AggregateValueExpression
:在AggregateExecutor中用到,作用和ColumnValueExpression
类似,用于找出属于当前column的这个值。(AggregateExecutor的测试函数在传入outputschme的时候,用于构造的colum不是ColumnValueExpression而是AggregateValueExpression,就是为了处理不同类型的输入参数。所以才能有以下的用法)说实话有点不太符合逻辑,而且不看测试文件根本不知道有这一用法。
Value value = column.GetExpr()->EvaluateAggregate(temp.Key().group_bys_, temp.Val().aggregates_);
Value
(最小数据单位)
Index
索引保存的是(Tuple_key, RID)
对,其中Tuple_key
是根据传入的Tuple
生成的。
也就是说索引是按照表中的每一行对应生成的。
在会对table造成修改的executor中会用到,通过提供的函数在修改table’的时候顺便把对应的indx修改。
数据存储三剑客
tuple
相当于表里面的一行,存储了一行value
。长度由shema
决定,每个column
可以在tuple
中对应一个value
// 就是将数据二进制化或者反过来,用于存储
void SerializeTo(char *storage) const;
void DeserializeFrom(const char *storage);
// return RID of current tuple
inline RID GetRid()
// 返回数据指针
inline char *GetData()
// 返回tuple的长度(bits)
inline uint32_t GetLength()
// 返回指定colum_idx位置的值
Value GetValue(const Schema *schema, uint32_t column_idx)
// 其他
Tuple KeyFromTuple(const Schema &schema, const Schema &key_schema, const std::vector<uint32_t> &key_attrs);
inline bool IsNull(const Schema *schema, uint32_t column_idx)
inline bool IsAllocated() { return allocated_; }
std::string ToString(const Schema *schema) const;
TableHeap
相当于一张表本身(represents a physical table on disk,just a doubly-linked list of pages.)
bool InsertTuple(const Tuple &tuple, RID *rid, Transaction *txn);
// MarkDelete标记需要删除的项,调用ApplyDelete删除
bool MarkDelete(const RID &rid, Transaction *txn);
void ApplyDelete(const RID &rid, Transaction *txn);
bool UpdateTuple(const Tuple &tuple, const RID &rid, Transaction *txn);
void RollbackDelete(const RID &rid, Transaction *txn);
// 获取Tuple
bool GetTuple(const RID &rid, Tuple *tuple, Transaction *txn);
//可以用迭代器访问Table
TableIterator Begin(Transaction *txn);
TableIterator End();
TableIterator
TableHeap的迭代器(指针),指向的是其中的Tuple,即可以当作Tuple指针来用。
数据表信息三剑客
Catalog
存储了一个数据库的全部表格信息的索引,提供了对表格的操作。
// TableInfo装载某个table的相关信息,包括schema,name,table指针,oid
TableInfo *GetTable(table_oid_t table_oid)
// IndexInfo 同理
IndexInfo *GetIndex(table_oid_t table_oid)
IndexInfo *CreateIndex(...)
IndexInfo *GetIndex(index_oid_t index_oid)
std::vector<IndexInfo *> GetTableIndexes(const std::string &table_name)
- TableInfo:装载某个table的相关信息
Schema schema_;
std::unique_ptr<TableHeap> table_;
const table_oid_t oid_;
const std::string name_;
- IndexInfo:类上
Schema key_schema_;
std::string name_;
std::unique_ptr<Index> index_;
index_oid_t index_oid_;
std::string table_name_;
const size_t key_size_;
schema
模式。每列都有一个表头(名字),shema就是一组表头的集合,拿来指明这张表有哪些列或者某个executer需要处理哪些列。
const Column &GetColumn(const uint32_t col_idx)
uint32_t GetColIdx(const std::string &col_name)
const std::vector<uint32_t> &GetUnlinedColumns()
uint32_t GetColumnCount()
uint32_t GetUnlinedColumnCount()
// 返回单个tuple的长度(不是schema的长度哦)
inline uint32_t GetLength()
// 返回是否内联(这里内联啥意思没懂,看代码反而是再判断是不是varchar)
inline bool IsInlined() const { return tuple_is_inlined_; }
column
相当于一列的名字(表头),也能调用其expression完成对应的操作(前面在AbstractExpression中有说)
std::string GetName()
TASK #1 - EXECUTORS
SEQUENTIAL SCAN
CPP问题
遍历表时,其中的每一个tuple都要经过plan_->GetPredicate()->Evaluate()判断是否满足条件。
但是我拿到的tuple其实是TableHeap的迭代器,看作是一个指向tuple的指针。
Value Evaluate(const Tuple *tuple, const Schema *schema)
根据Evaluate的参数,直接传入temp不行,因为temp不等同于tuple指针,只是重载了->
运算符。需要如下方式传入。
Value Evaluate(const Tuple *tuple, const Schema *schema)
plan_->GetPredicate()->Evaluate(&(*temp), &table_info_->schema_)
如果temp是普通的指针,那么&(*temp) 其实是等价于 temp。而temp作为迭代器肯定重载了*
运算符,的是Tuple本身,相当于指针使用*
解码。看TableIterator
声明,确实是。
const Tuple &operator*();
关于匹配列
测试案例有一项是SchemaChangeSequentialScan
,也就是说改变了OutSchema中column的名字。
也就是说名字不是判断column的唯一标识。
这里就要用到AbstructExpression
中提到的ColumnValueExpression
使用方法:可以从原始tuple中筛选出OutputSchema中需要的value。
for (const auto &column : plan_->OutputSchema()->GetColumns()) {
values.push_back(column.GetExpr()->Evaluate(&(*temp), &table_info_->schema_));
}
INSERT
根据我现在的理解,插入tuple更新索引时,只需要调用exec_ctx->GetIndex()->index_->InsertEntry()
就行。
索引保存的是(Tuple_key, RID)
对,其中Tuple_key
是根据传入的Tuple
生成的。
注意:InsertEntry
()第一个参数是Tuple类型的,但是如果直接传入Tuple,过不了测试,需要先转换成key值再传入(我寻思KeyFromTuple
这种函数就不能直接封装到InsertEntry
里面么, 用的时候传入原始Tuple不就行了,还得手动转化一下)
for (auto info : index_infos_) {
const auto index_key = tuple->KeyFromTuple(table_info_->schema_, info->key_schema_, info->index_->GetKeyAttrs());
info->index_->InsertEntry(index_key, *rid, exec_ctx_->GetTransaction());
}
UPDATE
类似INSERT
DELETE
类似INSERT
NESTED LOOP JOIN
原理
就是将两个孩子节点传回来的tuples两两经过谓词判断(就是判断指定columns位置的value是不是相等),将配对的tuple根据outputSchema组合成输出tuple。
所以刚好通过内外两层循环就可遍历。
吐槽
在SequentialPlan
中的predicate获取函数名是GetPredicate()
在NestedLoopJoinPlan中的predicate获取函数名又改成了Predicate()
…
疑问:左右child_executor是不是谁当outer_table都可以?
都可以,因为EvaluateJoin()
只分左右不分内外,反正都是两两匹配,内外循环都一样。
如何将内外俩tuple合并成输出tuple
for (uint32_t i = 0; i < plan_->OutputSchema()->GetColumnCount(); i++) {
values.emplace_back(plan_->OutputSchema()->GetColumn(i).GetExpr()->EvaluateJoin( &left_tuple_,
plan_->GetLeftPlan()->OutputSchema(),
&right_tuple_,
plan_->GetRightPlan()->OutputSchema()));
}
HASH JOIN
原理
其实就是简化版的 NESTED LOOP JOIN
思路是将其中一个表的所有tuple装入一个hash table(不用自己写,可以直接套用std::unorderd_map)
而有可能同一个key对应了多个tuple,所以map应该这样构造:
std::unordered_map<JoinKey, std::vector<Tuple>> left_table_map_;
对于每一个right tuple,判断其是否在map中有key值相同的项,如果有,还要考虑这一项是否有多个tuple。
(unorderd_map对于自己的类,需要封装==
和hash
,代码参考了这位大哥,这部分代码放在hash_join_executor.h
就行)
namespace bustub {
struct JoinKey {
Value value;
bool operator==(const JoinKey &other) const { return value.CompareEquals(other.value) == CmpBool::CmpTrue; }
};
} // namespace bustub
namespace std {
template <>
struct hash<bustub::JoinKey> {
std::size_t operator()(const bustub::JoinKey &agg_key) const {
size_t curr_hash = 0;
if (!agg_key.value.IsNull()) {
curr_hash = bustub::HashUtil::CombineHashes(curr_hash, bustub::HashUtil::HashValue(&agg_key.value));
}
return curr_hash;
}
};
} // namespace std
关于谓词
我一开始还在寻思,HashJoinPlan没有predicate,怎么来判断两个tuple是否能匹配?
转念一想,hash不就是只能拿来判断元素相等的情况么,HashJoin相当于默认谓词是:两tuple对应位置的value值相等。而利用hash表就是为了减小搜索的复杂度。
所以其实NESTED LOOP JOIN的谓词应该默认的是"=="
AGGREGATION
疑问
看AggregationPlan
的构造函数,group_bys,aggregates,agg_types 都是允许多个存在。我sql语法很差,一开始没怎么理解到为啥允许多个。
其实对于aggregates和agg_types,他俩个数是对应的,多个对对应了多个输出列,比如一列是max,一列是min,outputschema跟其有关系。
对于group_bys,每个group_by对应了一个列,只有一个group_by代表归一化一列中value相等的tuple,而多个group_by代表必须要多个列的value同时相等才能groupby。(这也不需要自己实现,多个groupby的情况已经在key的==
操作符重构中解决了)
不要忘了考虑having这个条件
当谓词来用就行了
Value value = has_having_ ?
plan_->GetHaving()->EvaluateAggregate(temp.Key().group_bys_, temp.Val().aggregates_)
: Value(TypeId::BOOLEAN, static_cast<int8_t>(true));
// has_having是用来排除没有传入having参数的情况
// has_having_ = plan_->GetHaving() != nullptr;
LIMIT
这个简单的好笑了
void LimitExecutor::Init() {
child_executor_->Init();
count_ = 0;
}
bool LimitExecutor::Next(Tuple *tuple, RID *rid) {
return count_++ < plan_->GetLimit() && child_executor_->Next(tuple, rid);
}
DISTINCT
直接照搬AGGREGATION的hash表实现,改个名字就行。
好累好累好累