CMU15445-project3-满分收获总结


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1hvKz8Ot-1658066439309)(C:\Users\54351\Desktop\Mynotes\CMU15445\CMU15445-lab3.assets\image-20220717205747950.png)]

排名拉了,没精力优化了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uj80SfBS-1658066439311)(C:\Users\54351\Desktop\Mynotes\CMU15445\CMU15445-lab3.assets\image-20220717205854345.png)]

太折磨了太痛苦了,看代码的时间比写代码的时间还多,很多天都处于完全没法动键盘的状态。

感觉这个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可以在这里获取TableOidPredicate

// 这个在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表实现,改个名字就行。


好累好累好累

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值