MiniOB Lab4: join tables & aggregation with group by

本次实验感觉挺难的,主要是需要了解的部分很多,理论又非常抽象,高度凝练的抽象与工程上的细节形成特别强烈的对比,很容易在工程的细节中迷失。这份记录希望能够帮助我理清楚相关的思路,虽然我到现在也还没有完全理解在执行过程中,miniob究竟做了什么。

Part I: Nested Loop Join

在这一部分我们首先需要完成的是最为简单的一种join的方式。我们先介绍一下查询的流程。我们将通过以下的SQL进行演示。

select * from join_table_1 inner join join_table_2 on join_table_1.id=join_table_2.id;

Query Processing

首先,对于SQL语句,经过Parse部分后,我们需要对算子和表达式进行Logical Plan,在将我们的算子进行逻辑计划后,优化形成控制流,最后转换成实际的物理执行计划。在写入结果时,数据从树的底部开始向上流动,根据物理算子进行操作,最后在树的根部获得结果。

这里,MiniOB采用的是火山模型。并不会实际地存储中间运算的结果,而是将中间运算结果层层往上传递直到树根。

接下来首先关注Logical plan。

Logical Plan

执行完语法分析后,sql将形成一个个sql_node,为后面的logical plan做好准备工作。在src/observer/net/sql_task_handler.cpp中,我们将在rc = optimize_stage_.handle_request(sql_event);中进入到逻辑树生成部分。

生成部分仍然是自上而下递归进行的。首先生成的逻辑操作,位于树根的就是我们的SELECT操作。因此我们将进入到select中继续向下生成逻辑树。

在进入到了select重载的Logical Plan中,我们将会检查有多少个Table节点。如果发现了多个Table节点,则意味着我们需要进行JOIN操作,因此生成join逻辑运算节点,将两个图节点加载到join的左右子节点下方。

随后判断是否具有谓词条件ON。如果有则设置。这在上图中的if中也可以看见。

随后select将判断是否还有剩余谓词和子查询。这里我们均没有出现,我们也将跳过。这样我们就生成了一个简单的逻辑谓词树。

Physical Plan

在经过优化后,我们将进入到物理计划生成部分。在这一部分中,如果我们已经部署了PAX存储结构和向量化处理方式,就可以采用向量化执行。但是这对于作业来说过于复杂,因此我们还是通过row存模式,一个一个tuple进行解决。这里我们将生成实际执行的算子。我们将依次进入 PROJECTION -> JOIN -> TABLE_GET 创建计划。





在每个计划中,我们将获得子节点。并递归式地创建计划,直到树的叶子节点。需要特别注意的是JOIN中的谓词和SQL语句中的谓词有着区别。一个是连接条件,一个是最终的筛选条件。因此我们文件中的predicate_*_.cpp/h不对JOIN语句产生作用,而是对SELECT产生作用。

这样我们形成的物理算子树:

自下而上数据的流动

我们在写入结果时才真正开始执行操作,如下图所示。

首先,我们需要自上而下打开算子。忽略掉网络通信算子,第一个打开的是projection算子,其次是nestedLoopJoin算子。

回想一下我们的物理算子,我们的物理算子有RESULT -> PROJECT -> NLJoin -> TABLE_GET



这样,我们在NLJ算子中就需要:

  • 获得子节点的两个算子。
  • 打开左表节点算子。

为什么不在NLJ算子中就打开右表节点?因为右表需要反复进行遍历。每一个left_tuple需要在遍历整个右表后才进入到下一个tuple。而从头遍历需要调用close,这样我们最好让右表遍历函数拥有对于右表的打开和关闭权限。否则很容易造成未定义行为(例如反复打开右表等)。

这样我们有:

RC NestedLoopJoinPhysicalOperator::open(Trx *trx) {
if (children_.size() != 2) {
return RC::INTERNAL;
} RC rc = RC::SUCCESS;
left_ = children_[0].get();
right_ = children_[1].get();
right_closed_ = true;
round_done_ = true; rc = left_->open(trx);
trx_ = trx;
return rc;
}

接下来我们进入到write_tuple_result。在这里,一个个tuple将自下而上传递,逐步完成物理算子操作。在next函数中递归进行控制,结果自下而上返回。

对于我们的NLJ操作,我们具有predicate谓词,也就是我们的ON子句。在这里,我们需要增加一个filter函数来过滤掉不符合我们的要求的tuple。

首先我们在头文件里增加:

private:
RC left_next(); //! 左表遍历下一条数据
RC right_next(); //! 右表遍历下一条数据,如果上一轮结束了就重新开始新的一轮
bool find_position(TupleSchema* schemas, TupleCellSpec& spec, int& index);
bool filter(); //! 过滤掉不符合条件的Descartes结果。

接下来我们需要完成双循环遍历的情况。我们可以注意到在头文件中的注释,告诉我们非常重要的部分:round_done_right_closed_,以及finished

首先我们需要思考双循环。我们有基础算法的伪代码如下:

  • 整理我们的思路。
  • 首先判断有没有获得left_tuple,也就是开始进入循环。进入到左节点的next获取一个tuple。
  • 接着进入到右节点获得一个tuple,直到右节点返回EOF。
  • 右节点返回EOF后,需要通过round_done_来表明右表一次遍历已经完成。同时通过关闭算子和打开算子的方式,将右节点设置到表的起点重新遍历。
  • 每次获得左边tuple和右边tuple后,需要判断是否保留该tuple。最简单的方式是不让next函数返回,这样中间值将会被下次迭代的结果替换。所以通过loop的方式来控制循环是否停止。停止则代表保留结果,写入sql_result。否则将不会停止,由新的结果来替换旧的结果。
RC NestedLoopJoinPhysicalOperator::next() {
if (finished) {
return RC::RECORD_EOF;
} RC rc;
bool loop = true;
while (loop) {
// 第一次开始,首先需要获得一个左表的tuple。
if (left_tuple_ == nullptr) {
rc = left_next();
if (rc != RC::SUCCESS) {
// 左表为空的时候结束。
finished = true;
return rc;
}
rc = right_next();
if (rc != RC::SUCCESS) {
// 右表为空的时候结束。(说明右表为空表)。
finished = true;
return RC::RECORD_EOF;
}
} else {
// 左表不变,获取新的右表tuple。
rc = right_next();
if (rc != RC::SUCCESS) {
// 右表一遍遍历完成,换一个新的左表tuple。
rc = left_next();
if (rc != RC::SUCCESS) {
finished = true;
return RC::RECORD_EOF;
}
// 右表从头开始遍历。
if (RC::SUCCESS != right_next()) {
finished = true;
return RC::RECORD_EOF;
}
}
}
// 过滤我们的tuple。
loop = filter();
}
return RC::SUCCESS;
} RC NestedLoopJoinPhysicalOperator::left_next() {
// 正常地获取一个tuple。
RC rc = RC::SUCCESS;
rc = left_->next();
if (rc != RC::SUCCESS) {
return rc;
} left_tuple_ = left_->current_tuple();
joined_tuple_.set_left(left_tuple_);
return rc;
} RC NestedLoopJoinPhysicalOperator::right_next() {
RC rc = RC::SUCCESS;
// 一轮是否已经结束?
if (round_done_) {
// 一轮结束,关闭右表。
if (!right_closed_) {
rc = right_->close();
right_closed_ = true;
if (rc != RC::SUCCESS) {
return rc;
}
}
// 重新打开右表,相当于回到起点。
rc = right_->open(trx_);
if (rc != RC::SUCCESS) {
return rc;
}
right_closed_ = false; round_done_ = false;
} // 获得右表的一个tuple。
rc = right_->next();
if (rc != RC::SUCCESS) {
// 当到达右表结尾时,返回EOF给上层next算子。
if (rc == RC::RECORD_EOF) {
round_done_ = true;
}
return rc;
} // 拼接右表。
right_tuple_ = right_->current_tuple();
joined_tuple_.set_right(right_tuple_);
return rc;
}

接下来就是filter部分。这一部分比较复杂,我们一步步地来了解。

predicates_

我们的predicates_是联结表达式,在expression.h中有这样的定义:

MINIOB里的表达式有哪些类型?

这样子表达式就应该是比较表达式。我们需要从连结表达式中获得子表达式,子表达式为比较表达式。比较表达式比较长,这里就通过代码块的形式展示。

/**
* @brief 比较表达式
* @ingroup Expression
*/
class ComparisonExpr : public Expression
{
public:
ComparisonExpr(CompOp comp, std::unique_ptr<Expression> left, std::unique_ptr<Expression> right);
virtual ~ComparisonExpr();
ExprType type() const override { return ExprType::COMPARISON; }
RC get_value(const Tuple &tuple, Value &value) const override;
AttrType value_type() const override { return AttrType::BOOLEANS; }
CompOp comp() const { return comp_; }
/**
* @brief 根据 ComparisonExpr 获得 `select` 结果。
* select 的长度与chunk 的行数相同,表示每一行在ComparisonExpr 计算后是否会被输出。
*/
RC eval(Chunk &chunk, std::vector<uint8_t> &select) override;
std::unique_ptr<Expression> &left() { return left_; }
std::unique_ptr<Expression> &right() { return right_; }
/**
* 尝试在没有tuple的情况下获取当前表达式的值
* 在优化的时候,可能会使用到
*/
RC try_get_value(Value &value) const override;
/**
* compare the two tuple cells
* @param value the result of comparison
*/
RC compare_value(const Value &left, const Value &right, bool &value) const;
template <typename T>
RC compare_column(const Column &left, const Column &right, std::vector<uint8_t> &result) const;
private:
CompOp comp_;
std::unique_ptr<Expression> left_;
std::unique_ptr<Expression> right_;
};

我们需要通过获取比较表达式,对left和right部分进行比较后,判断当前的join_tuples_是否符合条件,然后得到我们的结果。

  • Q1: 会出现什么情况呢?

我们举出的例子有:

select * from join_table_1 inner join join_table_2 on join_table_1.id=join_table_2.id;
select * from join_table_1 inner join join_table_2 on join_table_1.id=1;
select * from join_table_1 inner join join_table_2 on 1=join_table_2.id;
select * from join_table_1 inner join join_table_2 on 1=1;

我们发现,比较表达式的左右子表达式有以上四种情况:

  1. 左右表达式为field表达式。这两边需要从tuple中获取对应域的值进行比较。
  2. 左右表达式一个为field表达式,一个为Value表达式。从Value表达式中获取值,与对应的tuple中的域值进行比较。
  3. 左右表达式均为Value表达式

接下来我们需要了解怎么获取对应的索引,以及从tuple中获取对应的域值。这里我们通过find_position找到在tuple中对应的index。在函数签名中我们看到,我们也需要寻找到schema(格式信息),以及tupleCellSpec的相关信息。这里虽然不太知道这是干什么的,但是通过结构体中我们可以发现,这是记录 表名域名别名的结构体。因此我们需要从field表达式中获取域名和表名。field表达式,就是我们的table.field部分,他们与=共同构成了比较表达式,并绑定在join算子上。

bool NestedLoopJoinPhysicalOperator::find_position(TupleSchema *schemas,
TupleCellSpec &spec,
int &index)

同样,我们也有可能会出现一个不正常脑回路意外的查询,交换了左右表。例如:

select * from join_table_1 inner join join_table_2 on join_table_2.id=join_table_1.id;

这样就会导致find_position在只查询左表格式和左表对应tuple时失败。因此我们需要判断find_position是否恒真。否则反过来查询就可以了。

最后,通过比较表达式的compare_value来判断是否需要写入该结果 (也就是让next跳出循环返回)。在我们的循环条件中,loop为假时写入结果。因此当我们的比较值为真时,返回false。否则返回true,不写入结果。这样我们就有:

bool NestedLoopJoinPhysicalOperator::filter() {
bool ans = false;
// 要判断有没有on,否则会出现错误。
if (predicates_ == nullptr) {
return ans;
}
for (auto &pred : predicates_->children()) {
// 获得联结表达式下的比较表达式。
ComparisonExpr *cmp_pred = static_cast<ComparisonExpr *>(pred.get());
std::unique_ptr<Expression> &left = cmp_pred->left();
std::unique_ptr<Expression> &right = cmp_pred->right();
bool result = false;
Value left_val, right_val;
// 判断:是域表达式还是值表达式?随后根据情况比较。
if (left->type() == ExprType::FIELD && right->type() == ExprType::FIELD) {
FieldExpr *left_field = static_cast<FieldExpr *>(left.get());
FieldExpr *right_field = static_cast<FieldExpr *>(right.get()); const char *left_table_name, *left_field_name, *right_table_name,
*right_field_name;
left_table_name = left_field->field().table_name();
left_field_name = left_field->field().field_name();
right_table_name = right_field->field().table_name();
right_field_name = right_field->field().field_name(); TupleCellSpec left_tup(left_table_name, left_field_name);
TupleCellSpec right_tup(right_table_name, right_field_name); int left_index = 0, right_index = 0;
TupleSchema *left_schema = left_->schema();
TupleSchema *right_schema = right_->schema();
// 根据元数据信息取出对应Value进行比较。
if (!find_position(left_schema, left_tup, left_index)) {
find_position(right_schema, left_tup, left_index);
find_position(left_schema, right_tup, right_index);
joined_tuple_.left_tuple()->cell_at(right_index, right_val);
joined_tuple_.right_tuple()->cell_at(left_index, left_val);
} else {
joined_tuple_.left_tuple()->cell_at(left_index, left_val);
joined_tuple_.right_tuple()->find_cell(right_tup, right_val);
}
} else if (left->type() == ExprType::FIELD &&
right->type() == ExprType::VALUE) {
int index = 0;
FieldExpr *left_field = static_cast<FieldExpr *>(left.get()); const char *table_name, *field_name;
table_name = left_field->field().table_name();
field_name = left_field->field().field_name();
TupleCellSpec tuple(table_name, field_name);
TupleSchema *schema = left_->schema();
if (!find_position(schema, tuple, index)) {
schema = right_->schema();
find_position(schema, tuple, index);
joined_tuple_.right_tuple()->cell_at(index, left_val);
} else {
joined_tuple_.left_tuple()->cell_at(index, left_val);
}
static_cast<ValueExpr *>(right.get())->get_value(right_val);
} else if (left->type() == ExprType::VALUE &&
right->type() == ExprType::FIELD) {
int index = 0;
FieldExpr *right_field = static_cast<FieldExpr *>(right.get());
const char *table_name, *field_name;
table_name = right_field->field().table_name();
field_name = right_field->field().field_name(); TupleCellSpec tuple(table_name, field_name);
TupleSchema *schema = left_->schema();
if (!find_position(schema, tuple, index)) {
schema = right_->schema();
find_position(schema, tuple, index);
joined_tuple_.right_tuple()->cell_at(index, left_val);
} else {
joined_tuple_.left_tuple()->cell_at(index, left_val);
}
static_cast<ValueExpr *>(left.get())->get_value(right_val);
} else {
auto left_val_expr = static_cast<ValueExpr *>(left.get());
auto right_val_expr = static_cast<ValueExpr *>(right.get());
left_val_expr->get_value(left_val);
right_val_expr->get_value(right_val);
}
cmp_pred->compare_value(left_val, right_val, result);
// 注意!结果不相等时返回true,不让该tuple写入。
if (!result) {
ans = true;
break;
}
}
return ans;
}

这样Lab1的部分就完成了。

Part II: Aggrregate & group by

我们首先看一下我们的explain,了解一下过程。

同样地,首先我们需要进入到logical plan中。在这里,我们的relation只有一个,因此不会出现join算子。接下来,我们需要进入到group by逻辑算子中生成logical plan。

在group by的逻辑生成过程中,我们将会解析group by对象having 子句聚合表达式,以及子查询。随后将他们检查并进行绑定。

在hash_group_by中,我们的执行步骤如同手册上所说:



这样我们需要补充的部分就是:1. 获取group by后得到的复合tuple。2. 根据having子句进行筛选。(我们可以启用被注释掉的filter函数)。这样我们就有:

RC HashGroupByPhysicalOperator::open(Trx *trx)
{
if (always_false) {
return RC::SUCCESS;
}
ASSERT(children_.size() == 1, "group by operator only support one child, but got %d", children_.size()); PhysicalOperator &child = *children_[0];
RC rc = child.open(trx);
if (OB_FAIL(rc)) {
LOG_INFO("failed to open child operator. rc=%s", strrc(rc));
return rc;
} ExpressionTuple<Expression *> group_value_expression_tuple(value_expressions_); ValueListTuple group_by_evaluated_tuple; while (OB_SUCC(rc = child.next())) {
Tuple *child_tuple = child.current_tuple();
if (nullptr == child_tuple) {
LOG_WARN("failed to get tuple from child operator. rc=%s", strrc(rc));
return RC::INTERNAL;
} // 找到对应的group
GroupType *found_group = nullptr;
rc = find_group(*child_tuple, found_group);
if (OB_FAIL(rc)) {
LOG_WARN("failed to find group. rc=%s", strrc(rc));
return rc;
} // 计算需要做聚合的值
group_value_expression_tuple.set_tuple(child_tuple); // 计算聚合值
GroupValueType &group_value = get<1>(*found_group);
rc = aggregate(get<0>(group_value), group_value_expression_tuple);
if (OB_FAIL(rc)) {
LOG_WARN("failed to aggregate values. rc=%s", strrc(rc));
return rc;
}
} if (RC::RECORD_EOF == rc) {
rc = RC::SUCCESS;
} if (OB_FAIL(rc)) {
LOG_WARN("failed to get next tuple. rc=%s", strrc(rc));
return rc;
} // 得到最终聚合后的值 for (GroupType &group : groups_) {
GroupValueType &group_value = get<1>(group);
rc = evaluate(group_value);
if (OB_FAIL(rc)) {
LOG_WARN("failed to evaluate group value. rc=%s", strrc(rc));
return rc;
}
// 这里开始!首先我们需要获得聚合(evaluate)后的值。这里可以阅读group by内的实现。
CompositeTuple& tuple = get<1>(group_value);
bool result = false;
rc = filter(tuple, result);
if (rc != RC::SUCCESS) {
LOG_TRACE("record filtered failed=%s", strrc(rc));
return rc;
}
if (result)
results_.emplace_back(std::move(group));
}
groups_.swap(results_);
current_group_ = groups_.begin();
first_emited_ = false;
return rc;
}

这里我们需要补充std::get函数。这是一个从tuple中获取对应类型/对应index的元素的函数。在这里,我们的GroupValueType是一个元组。我们有:

using GroupValueType = std::tuple<AggregatorList, CompositeTuple>;

这样我们可以通过std::get<1>来获取CompositeTuple。自C++14起,我们也可以通过std::get<CompositeTuple>来获取我们的复合tuple。接下来不要忘记通过having子句进行过滤。这里的having子句和predicate子句的next操作几乎完全相同,都是对其他算子得到的结果进行筛选。直接对着抄就好。

RC HashGroupByPhysicalOperator::filter(CompositeTuple &tuple, bool &result)
{
RC rc = RC::SUCCESS;
Value value;
// 根据having子句中的值进行筛选。
for (unique_ptr<Expression> &expr : havings_) {
rc = expr->get_value(tuple, value);
if (rc != RC::SUCCESS) {
return rc;
} bool tmp_result = value.get_boolean();
if (!tmp_result) {
result = false;
return rc;
}
} result = true;
return rc;
}

这样就完成了我们的having子句。

MiniOB Lab4: join tables & group by的更多相关文章

  1. 在mysql中RIGHT JOIN与group by一起使用引起的一个大bug

    本来按理说这个小问题不值得写一个博客的,不过正是这个小问题造成了一个大bug. 本来每月对数据都好好的,但是这一两天突然发现许多数据明显不对,这一块的代码和sql有些不是我写的,不过出现了bug,还是 ...

  2. linq-to-sql实现left join,group by,count

    linq-to-sql实现left join,group by,count 用linq-to-sql实现下面的sql语句: SELECT p.ParentId, COUNT(c.ChildId) FR ...

  3. sql、linq和lambda查询语句比较inner join和group by组合使用及匿名类型的处理

    使用EF自己做的小功能需要遇到inner join和group by组合使用及匿名类型的处理,搜了很多,基本不能满足自己的需要,所以总结了也实现了就自己写出来,已备查看及伙伴查询参考(一般的语句查询就 ...

  4. linq多表join与group

    var query =from a in this.ObjectContext.siteInfo join b in this.ObjectContext.shopInfo on a.siteID e ...

  5. EF 里的 join and Group Join

    join ); pageCount = _db.Orders.Count(); return _db.Orders.OrderByDescending(c=>c.ID).Skip(skip).T ...

  6. SQL 笔记1,left join,group by,having

    表:XS,XK,CJ left join 表1 on 表1.字段=表2.字段 group by 分组条件 order by 排序条件 asc正序(小到大),desc倒序 having 跟条件类似whe ...

  7. LINQ学习笔记 Join 与 Group join

    LINQ中的Join对应T-SQL中的内连接,并无左连接的方法,当然也没有右连接. 要达成Left join必须依靠GroupJoin来完成. GroupJoin顾名思义就是先集团在做加入,加入的不同 ...

  8. C# Lambda Left Join AND Group by Then Sum

    var list = List1.Join( List2, l1 => new { l1.Cityid }, l2 => new { l2.Cityid }, (item1, item2) ...

  9. Mysql 下 Insert、Update、Delete、Order By、Group By注入

    Insert: 语法:INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....) 报错注入: insert into test(id,name,p ...

  10. MySQL自学笔记_联结(join)

    1.  背景及原因 关系型数据库的一个基本原则是将不同细分数据放在单独的表中存储.这样做的好处是: 1).避免重复数据的出现 2).方便数据更新 3).避免创建重复数据时出错 例子: 有供应商信息和产 ...

随机推荐

  1. sox 改变频率

    http://blog.csdn.net/chenxieyy/article/details/51506766 1,将2.wav文件的采样率修改为16000HZ 命令如下:sox   2.wav   ...

  2. Delaunay 三角化 学习3

    简介 还是太菜从解析官方代码开始. 官方代码共有三个核心函数. 参考链接 http://blog.sina.com.cn/s/blog_6029f0330101irlh.html http://pau ...

  3. 一种基于openmv和触摸屏的动态读取颜色的解决方案

    一种基于openmv和触摸屏的动态读取颜色的解决方案 前言: ​ 作为大学生电子设计竞赛控制题选手,常常需要与视觉上位机打交道,openmv作为当前一种开源方案,能够以较低的成本制作,并且官方文档和各 ...

  4. 20212109 实验二《Python程序设计》实验报告

    实验二 计算器设计 # 20212109 2021-2022-2 <Python程序设计>实验二报告课程:<Python程序设计>班级: 2121姓名: 施铖哲学号:20212 ...

  5. Win10正式版如何查看电脑Wifi密码的问题

    一些电脑基地的用户,说从Win10正式版计算机中如何提取已经保存的Wi-Fi密码?其实,这个还是很简单的,下面,技术员小编就来分享具体的提取方法. 在 Windows 10 中,查看已保存的 Wi-F ...

  6. jar-protect 代码加壳加密工具【开源】

    开源地址:https://gitee.com/chejiangyi/jar-protect 介绍 java 本身是开放性极强的语言,代码也容易被反编译,没有语言层面的一些常规保护机制,jar包很容易被 ...

  7. 有了WriteRightly 这个AI神器,大学里的混子导师可以失业了

    有了这个AI论文神器,大学里的"混子导师"都可以失业了 七月毕业季.同学们不是在毕业旅行就是已经准备新工作了. 而我研三的表弟,学计算机的,却仍然在图书馆改毕业论文.一周前我舅,也 ...

  8. 我只说需求,AI 全程托管,代码自己长出来了!

    你是否想过,有一天开发可以像"点外卖"一样简单?你只管提出想法,剩下的从需求分析.代码编写.调试测试到最终部署,都由一个"超级程序员"全自动搞定. 这不是科幻, ...

  9. Boost升压PFC旁路二极管

    如图,为Boost升压PFC常用实用拓扑(增加预充电路和旁路二极管) 如下为 一.上电瞬间 1.防止母线冲击电流过大,PFC电路一般有预充电电路,预充电阻会限流. PFC电感L开机浪涌电流产生左+右- ...

  10. MyEMS能源管理系统后台配置-设备管理

    MyEMS开源能源管理系统适用于建筑.工厂.商场.医院.园区的电.水.气等能源数据采集.分析.报表,还有光伏.储能.充电桩.微电网.设备控制.故障诊断.工单管理.人工智能优化等可选功能. 本文介绍My ...