P3 查询

news2024/11/19 5:54:34

文章目录

    • Task1 访问方法执行程序
      • seq_scan_executor
      • insert_executor
      • update_executor
      • delete_executor
      • index_scan_executor
    • Task2 聚合和连接执行器
      • Aggregation
      • NestedLoopJoin
      • HashJoin
      • 优化NestedLoopJoin到HashJoin
    • Task3 排序+限制执行器和Top-N优化
      • Sort
      • Limit
      • Top-N优化规则

BusTub架构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可见,Project3并不负责解析SQL语句,而是在执行计划已经形成之后,填充计划结点对应的执行过程(Query Execution)并做优化(Optimizer)

tools/shell/shell.cppmain()为开始函数,调用linenoise工具负责命令行解析,两层死循环,内层负责逐词拼装sql,外层解析执行等。之后是Parser,使用了Postgers的libpg_query进行SQL解析。解析产生的Postgers解析树经过binder转换为可以被BusTub规划器明确识别的绑定树。Planner接受绑定树,并将其转换为BusTub计划树。执行引擎将使用计划树来执行语句。Optimizer接受计划树,然后对它进行结构优化,从而加快DBMS执行效率。

之后就是执行,执行需要执行引擎ExecutionEngine,执行引擎的工作就是调用执行器工厂方法ExecutorFactory::CreateExecutor()根据优化后的计划树拼接出对应的执行器,具体就是通过直接递归从计划树的根到叶,最终生产出"执行器树",之后执行引擎PollExecutor()轮询"执行器树",说是轮询其实就是不停调用"执行器树"的根执行器的Next(),此时的轮询是因为执行器的Next()一次只能返回一条记录,为了获得完整的结果只能轮询根执行器,而对非根执行器的Next()调用是在根执行器的Next()中完成的(Init()同理),换句话说,请求从根执行器下去,一层一层到达叶执行器,叶执行器返回一条记录,又经过一层层执行器处理,最后到达根执行器得到一条执行结果。这就是火山模型——记录不停向上冒。

这样结合架构图看,我们只需要填充Executors中各个执行器即可。

Task1 访问方法执行程序

在本任务中,您将实现对存储系统中的表进行读写的执行器。

seq_scan_executor

SeqScanExecutor 遍历表并每次返回一个元组

//SeqScanExecutor类私有属性
/** 需要执行的顺序扫描计划节点 */
const SeqScanPlanNode *plan_;

std::unique_ptr<TableIterator> it_;  // 表的迭代器
IsolationLevel isl_;                 // 事务隔离级别

SeqScanExecutor::Init()

SeqScanExecutor::Next(),叶执行器不需要再执行子执行器的Next(),先加行级读写锁,通过表迭代器获取一对数据it_->GetTuple();,数据类型为std::pair<TupleMeta, Tuple>pair可以理解成两个返回值的封装,Tuple是记录的相关信息,TupleMeta是记录的事务id和是否删除的标记的结构体。若未删除就取出记录信息,将记录信息和获取的RID(即记录在硬盘中的位置)赋给传入的参数,以供父执行器使用。之后解锁,表迭代器自增,指向下一个记录,其核心就是auto next_tuple_id=rid_.GetSlotNum()+1;,在此之前需要获取当前页并强转为TablePage,目的是获取现存记录数,从而确定迭代器是否要换页,slot_num_表示记录在RID标记的页的偏移量。

*tuple = pair.second;
*rid = tuple->GetRid();

表迭代器TableIterator有对表堆TableHeap的引用,表堆表示磁盘上的物理表

//TableHeap的私有属性
BufferPoolManager *bpm_;
page_id_t first_page_id_{INVALID_PAGE_ID};

std::mutex latch_;
page_id_t last_page_id_{INVALID_PAGE_ID}; /* protected by latch_ */

关于加解锁的问题后面统一分析。

insert_executor

InsertExecutor 将元组插入到表中,并更新任何受影响的索引。它只有一个子节点,生成要插入到表中的值。规划器 planner 将确保这些值具有与表相同的模式。执行器将生成一个整数类型的元组作为输出,表示有多少行已插入到表中。如果有与表关联的索引,请记住在插入表时更新索引。

//InsertExecutor类私有属性
/** 要执行的插入计划节点 */
const InsertPlanNode *plan_;

std::unique_ptr<AbstractExecutor> child_;  // 这个是insert节点的子节点,也就是value节点,它一次产生一个tuple
const TableInfo *table_;//表的元信息
std::vector<IndexInfo *> indexs_;
bool is_end_;

InsertExecutor::Init(),锁表,获取表和表索引。身为中间/根执行器,需要调用子执行器的Init()

InsertExecutor::Next(),进入循环,条件是子处理器(其实是ValuesExecutor)的Next()返回true,因为可能要插入多个记录。通过table_调用TableHeap::InsertTuple()进行记录插入,实际是插入到了BufferPoolManager的页中,获取返回的插入的记录所在的RID,下一段代码是将插入动作添加到事务的撤销表中,以防回滚

auto twr = TableWriteRecord(table_->oid_, *rid, table_->table_.get());  // 写记录
twr.wtype_ = WType::INSERT;
exec_ctx_->GetTransaction()->AppendTableWriteRecord(twr);  // 插入写记录

之后更新所有的表索引,先调用Tuple::KeyFromTuple()获取新插入的元组的key,然后调用表索引的InssertEntry()插入新元组的索引,然后同样将插入新索引的动作添加到事务的索引写集中,以防回滚

auto iwr = IndexWriteRecord(
						*rid, 
						table_->oid_, 
						WType::INSERT, 
						key_tuple, index->index_oid_, 
						exec_ctx_->GetCatalog());      // 索引记录
exec_ctx_->GetTransaction()->AppendIndexWriteRecord(iwr);  // 增加索引记录

最后将插入记录数封装成一个新元组,赋给传入参数,返回结束

update_executor

UpdateExecutor 可以修改指定表中的现有元组。执行器将生成一个整数类型的元组作为输出,指示更新了多少行。记住要更新受update影响的所有索引。

UpdateExecutor的子节点一般为Filter或者SeqScan,plan_->target_expressions_包含待更新的值

Next()循环中先遍历plan_->target_expressions_中所有的表达式,并根据表达式产生一行新元组values.emplace_back(expr->Evaluate(tuple, table_info_->schema_));,这种表达式如:SET colB=10时,target_expressions_[#0.0, 10, #0.2, #0.3]

对于 target_exprs=[#0.0, 15445, #0.2, #0.3],target_exprs 数组的第二个表达式是ConstantValue 表达式,直接产生一个整数值;其余三个都是列值表达式,从一个元组的对应列分别得到值。

这样在遍历完后先将旧记录的元信息标记删除,然后直接调用TableHeap::InsertTuple()插入新元组,后续同插入一样:记录新元组的位置、更新所有索引(获取旧和新元组的key,先DeleteEntry()删除再InsertEntry()插入)、封装更新记录数返回

delete_executor

只有一个子节点,子节点包含要从表中删除的记录。您的删除执行器应该生成一个整数输出,表示它从表中删除的行数。它还需要更新所有受影响的索引。

可以假设 DeleteExecutor 总是在它出现的查询计划的根位置。DeleteExecutor 不应该修改它的结果集。

Next()循环中拿到要删除的记录的RID,然后table_info_->table_->GetTupleMeta(next_rid);获得记录的元信息,将is_deleted置true后更新到table_info_中表示被删除了。创建TableWriteRecord对象,表示为删除动作,然后插入到事务的表写表中。遍历indexs_更新所有索引DeleteEntry(),并创建IndexWriteRecord对象插入到事务的索引写集中,封装删除条数返回。

index_scan_executor

IndexScanExecutor 遍历索引以检索元组的’ rid '。操作符然后使用这些 RID 在相应的表中检索它们的元组。然后,它一次一个地发出这些元组。

using BPlusTreeIndexForTwoIntegerColumn = BPlusTreeIndex<IntegerKeyType, IntegerValueType, IntegerComparatorType>;

// IndexScanExecutor类的私有属性
/** The index scan plan node to be executed. */
const IndexScanPlanNode *plan_;

IndexInfo *index_info_;                    // 具体的列的索引
BPlusTreeIndexForTwoIntegerColumn *tree_;  // b+树索引
IndexIterator<IntegerKeyType, IntegerValueType, IntegerComparatorType> iter_;
IndexIterator<IntegerKeyType, IntegerValueType, IntegerComparatorType> end_;

一个索引就是一个B+树,两个索引迭代器初始化如下:

iter_ = tree_->GetBeginIterator();//取树的叶子链表的左端
end_ = tree_->GetEndIterator();//取叶子链表的右端

Next()中两个迭代器也不过是对B+树的叶子链表进行遍历获取同一索引列的不同行RID而已,注意外部循环的作用是跳过is_deleted_标记的已删除行,每次调Next()只能返回一行的元组。

Task2 聚合和连接执行器

Aggregation

实现聚合的一种常用策略是使用散列表,以group-by列作为键。在这个项目中,您可以假设聚合哈希表适合内存。这意味着您不需要实现一个多阶段、基于分区的策略,并且哈希表不需要由缓冲池页面支持。

聚合执行器为每组输入计算一个聚合函数。它只有一个子节点。输出结果由grup-by的列紧接着是聚合列组成。

//AggregationExecutor的私有属性
/** The aggregation plan node */
const AggregationPlanNode *plan_;

/** 子执行器,它产生计算聚合的元组 */
std::unique_ptr<AbstractExecutor> child_executor_;

/** 简单聚合hash表 */
SimpleAggregationHashTable aht_;

/** Simple aggregation hash table iterator */
SimpleAggregationHashTable::Iterator aht_iterator_;
SimpleAggregationHashTable::Iterator aht_iterator_end_;

AggregationExecutor::Init()初始化子执行器、聚合哈希表,并在这里执行子执行器的Next(),调用MakeAggregateKey()MakeAggregateValue()获得子执行器获得的记录在哈希表中的k-v,然后传入InsertCombine()将k-v插入哈希表同时和之前聚合运算的结果进行聚合运算CombineAggregateValues()。若运算的是空表,就调用InitInsert()初始化插入。

SimpleAggregationHashTable实质上是一个unordered_map,键是分组列(GROUP BY后列名)产生的,值是聚合列(聚合函数的参数列)产生的,map中的键不重复,调用count()就可以查到有几个分组列,而map中存的也不是聚合列产生的AggregateValue,而是聚合结果的数组(可能有多个聚合函数),数组中某个值就是当前分组的某个聚合操作的当前结果。这样在哈希表中天然地做到了分组,在CombineAggregateValues()中和已有的计算数据进行聚合运算。

//SimpleAggregationHashTable私有属性
/** 哈希表只是从聚合键到聚合值的映射 */
std::unordered_map<AggregateKey, AggregateValue> ht_{};  // 聚合键和其对应的桶
/** 我们有的聚合表达式,即MAX(...),SUM(...)之类的 */
const std::vector<AbstractExpressionRef> &agg_exprs_;
/** 聚合类型集合 */
const std::vector<AggregationType> &agg_types_;

CombineAggregateValues()传入哈希表中分组列的位置&ht_[agg_key]和聚合列产生的AggregateValue。遍历聚合函数列表(SELECT后有多个聚合函数)进行switch-case选择聚合类型,每进一次该函数说明有一条记录需要进行聚合操作。

AggregationExecutor::Next()每次调用都返回一个分组和它的聚合结果插在一起的vector

std::vector<Value> values;  // 注意输出模式
values.insert(values.end(), aht_iterator_.Key().group_bys_.begin(), aht_iterator_.Key().group_bys_.end());
values.insert(values.end(), aht_iterator_.Val().aggregates_.begin(), aht_iterator_.Val().aggregates_.end());

封装成Tuple后哈希表迭代器自增,指向下一个分组。

NestedLoopJoin

默认情况下,DBMS将对所有连接操作使用NestedLoopJoinPlanNode。

您需要使用类中的简单嵌套循环连接算法为 NestedLoopJoinExecutor 实现内连接和左连接。该操作符的输出模式是左表的所有列,然后是右表的所有列。对于外部表中的每个元组,考虑内部表中的每个元组,如果满足连接谓词,则发出一个输出元组。

嵌套循环连接就类似SELECT * FROM a, b;这样,对一个表的循环中对另一个表进行循环匹配。

/** The NestedLoopJoin plan node to be executed. */
const NestedLoopJoinPlanNode *plan_;

std::unique_ptr<AbstractExecutor> left_executor_;
std::unique_ptr<AbstractExecutor> right_executor_;

Tuple left_tuple_;//左表循环时的当前行
Tuple right_tuple_;//右表循环时的当前行
bool is_match_;//标识在一次右表循环中是否至少有一次匹配

Next()每次调用产生一个左表的一行和匹配的右表某行的一个列表,无匹配则右表部分是空值:

//左表           //右表
-------------   -------------
|  a  |  b  |   |  c  |  d  |
-------------   -------------
|  1  |  n  |   |  1  |  x  |
-------------   -------------
                |  1  |  y  |
                -------------
//一次Next()的结果
|  1  |  n  |  1  |  x  |

Next()进行一个死循环,目的是循环左表if (!left_executor_->Next(&left_tuple_, rid)) return false;,之后进入内层循环,即右表循环,这样就是简单嵌套循环连接。内层循环中调用plan_->Predicate()->EvaluateJoin(...);来判断两个行记录是否匹配,可见是根据执行计划中的谓词来判断的,谓词即表达式,谓词表现为一个表达式树,这样,底层结点如ColumnValueExpression就调用GetValue()来获得元组中特定列的值。

//ComparisonExpression::EvaluateJoin()
auto EvaluateJoin(const Tuple *left_tuple, const Schema &left_schema, 
                  const Tuple *right_tuple,const Schema &right_schema) const -> Value override {
    Value lhs = GetChildAt(0)->EvaluateJoin(left_tuple, left_schema, 
                                            right_tuple, right_schema);//左子树
    Value rhs = GetChildAt(1)->EvaluateJoin(left_tuple, left_schema, 
                                            right_tuple, right_schema);//右子树
    return ValueFactory::GetBooleanValue(PerformComputation(lhs, rhs));//switch比较
}

如果判断匹配,调用GetOutputTuple()并告知已匹配,获取返回的一行数据后返回真,从而让执行引擎继续轮询。

NestedLoopJoinExecutor::GetOutputTuple()通过执行计划获得左右表的列数,先遍历tuple的各列将左表的一行添加进vector,若匹配成功,就同样将右表的一行添加进vector,封装成Tuple,私有属性is_match_置true。

实际上,当右表没有记录和左表的一行匹配时,即在内部循环结束时要返回的vector还没有准备好,于是在外部循环中、内部循环之后添加判断is_match_,若为false则需要在要返回的vector的右表部分插右表列数个空值,调用GetOutputTuple(tuple, false);,之后调用right_executor_->Init();来初始化右表迭代器,让右表从头再来,返回true。

//SeqScanExecutor::Init()
it_ = std::make_unique<TableIterator>(table->table_->MakeEagerIterator());// 在init中初始化,后面loop join会调用

之后就是左表的下一个,即left_executor_->Next(...),最后初始化右表迭代器,并恢复is_match_

HashJoin

如果查询包含两个列之间相等条件结合的连接(相等条件用“AND”分隔),DBMS可以使用HashJoinPlanNode。

您需要使用类中的散列连接算法实现 HashJoinExecutor 的内连接和左连接。该操作符的输出模式是左表的所有列,然后紧接着是右表的所有列。与聚合一样,您可以假设连接使用的哈希表完全适合内存。

哈希连接主要分为两个阶段:建立阶段(build phase)和探测阶段(probe phase)

Bulid Phase

选择一个表(一般情况下是较小的那个表,以减少建立哈希表的时间和空间),对其中每个元组上的连接属性(join attribute)采用哈希函数得到哈希值,从而建立一个哈希表。

Probe Phase

对另一个表,扫描它的每一行并计算连接属性的哈希值,与bulid phase建立的哈希表对比,若有落在同一个bucket的,如果满足连接谓词(predicate)则连接成新的表。

在内存足够大的情况下建立哈希表的过程时整个表都在内存中,完成连接操作后才放到磁盘里。但这个过程也会带来很多的I/O操作。

哈希连接(hash join)原理介绍 - 知乎 (zhihu.com)

//HashJoinExecutor私有属性
/** The NestedLoopJoin plan node to be executed. */
const HashJoinPlanNode *plan_;
std::unordered_map<JoinHashKey, JoinHashValue> ht_{};//哈希表,JoinHashValue本质是一个vector
std::unique_ptr<AbstractExecutor> left_child_;
std::unique_ptr<AbstractExecutor> right_child_;

std::vector<Tuple> match_right_tuples_;  // 左右哈希键都有可能重复
Tuple left_tuple_;
//HashJoinPlanNode属性
/** 用于计算左JOIN键的表达式 */
std::vector<AbstractExpressionRef> left_key_expressions_;//left join key,即左连接表达式
/** 用于计算右JOIN键的表达式 */
std::vector<AbstractExpressionRef> right_key_expressions_;
/** The join type */
JoinType join_type_;

//SELECT * FROM test_1 JOIN test_2 ON test_1.colA = test_2.colB;
//左连接表达式 test_1.colA
//右连接表达式 test_2.colB

hashjoin

Init()将左右子执行器初始化,遍历右子执行器对应的表while (right_child_->Next(&tuple, &rid)) {,我们期望它是小表,这样哈希连接的性能才能发挥出来。循环内,获取右连接表达式,遍历表达式列表,通将右表当前元组的连接列的值插入到JoinHashKey结构中(其实就是一个vector)形成哈希表中的一个key,将当前元组插入到该key对应的JoinHashValue(也是一个vector),这样就完成哈希表的构建了。

Next()死循环,是为了一个左表元组匹配多个右表元组(即"遍历"JoinHashValue)。和NestedLoopJoin一样,执行引擎每次调用Next()都会获得一条左表与右表匹配的结果,但是为了遍历JoinHashValue,必须在两次调用间保存这个数组,所以有了match_right_tuples_,因此Next()死循环开头检查match_right_tuples_,有就直接当作下一个匹配的右表元组取出、拼接并返回,而上一个调用需要返回Next()开头执行检查代码,所以有了这个死循环。其他逻辑与NestedLoopJoin相同。

HashJoinExecutor::GetOutputTuple()NestedLoopJoinExecutor::GetOutputTuple()相似,返回模式也一样。

优化NestedLoopJoin到HashJoin

哈希连接通常比嵌套循环连接产生更好的性能。当可以使用散列连接时,您应该修改优化器,将’ NestedLoopJoinPlanNode ‘转换为’ HashJoinPlanNode '。特别是,当连接谓词是两列之间的相等条件的连接时,可以使用散列连接算法。为了这个项目的目的,处理一个相等条件,以及两个由"AND"连接的相等条件,将获得全额学分。

优化器规则实现指南
BusTub优化器是一个基于规则的优化器。大多数优化器规则以自下而上的方式构建优化计划。由于查询计划具有这种树结构,因此在将优化器规则应用于当前计划节点之前,您需要首先将这些规则递归地应用于它的子节点。

在每个计划节点上,您应该确定源计划结构是否与您试图优化的计划结构匹配,然后检查该计划中的属性,以查看是否可以将其优化为目标优化的计划结构。

在公共BusTub存储库中,我们已经提供了几个优化器规则的实现。请参考一下。

可以把优化器想象成一层层的优化层,每一个执行计划结点产生都经过所有优化层:

//optimizer_custom_rules.cpp
auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
    auto p = plan;
    p = OptimizeMergeProjection(p);
    p = OptimizeMergeFilterNLJ(p);
    p = OptimizeNLJAsHashJoin(p);//待实现的优化函数
    p = OptimizeOrderByAsIndexScan(p);
    p = OptimizeSortLimitAsTopN(p);
    return p;
}

Optimizer::OptimizeNLJAsHashJoin()支持一个比较连接条件和由AND连接的两个比较连接条件,效果是传入一个NestedLoopJoinPlanNode传出一个分析好连接条件的HashJoinPlanNode,所以代码的核心工作就是通过传入的计划结点的谓词分析出哈希连接所需的左计划结点、右计划结点、左连接表达式数组(test_1.colA)、右连接表达式数组、连接类型。

先递归执行传入的计划结点的子节点们的优化,然后将传入的计划结点强转成NestedLoopJoinPlanNode,之后两个并列的大if分别处理没有and和有and的情况,有and和没有and的判断方法是将计划结点的谓词分别强转成逻辑表达式LogicExpression和比较表达式ComparisonExpression是否成功。

没有and时,我们只关心等值谓词,expr->comp_type_ == ComparisonType::Equal,取出比较表达式的左值和右值,分别构建ColumnValueExpression类型的左右表达式的共享指针,然后加到vector中,这是为了和有and的情况下一边有多个表达式相兼容。

std::vector<AbstractExpressionRef> l{left_expr_tuple_0};
std::vector<AbstractExpressionRef> r{right_expr_tuple_0};

通过ColumnValueExpression::GetTupleIdx()获得该表达式来自连接(“JOIN”)的哪一侧,之后返回构建的HashJoinPlanNode型共享指针。

//left_expr来自连接左侧
if (left_expr->GetTupleIdx() == 0 && right_expr->GetTupleIdx() == 1) {
    return std::make_shared<HashJoinPlanNode>(
        nlj_plan.output_schema_, 
        nlj_plan.GetLeftPlan(),
        nlj_plan.GetRightPlan(), 
        l, 
        r, 
        nlj_plan.GetJoinType());
}
//left_expr来自连接右侧
if (left_expr->GetTupleIdx() == 1 && right_expr->GetTupleIdx() == 0) {
    return std::make_shared<HashJoinPlanNode>(
        nlj_plan.output_schema_,
        nlj_plan.GetLeftPlan(),/*左计划结点*/
        nlj_plan.GetRightPlan(),/*右计划结点*/
        r, /*左连接表达式数组*/
        l, /*右连接表达式数组*/
        nlj_plan.GetJoinType());/*连接类型*/
}

之后是处理and,因为只会有一个and所以可以直接取左右两个表达式强转成ComparisonExpression,只需处理两边都是相等条件的情况,哈希连接处理等值连接条件最方便,之后的流程和无and一样:取出比较表达式的左值和右值、分别构建ColumnValueExpression类型的左右表达式的共享指针、加到vector中、根据GetTupleIdx()安放连接表达式数组。

Task3 排序+限制执行器和Top-N优化

Sort

如果查询的’ ORDER BY '属性与索引的键不匹配,BusTub将为如下查询生成一个’ SortPlanNode ’

该计划节点具有与其输入模式相同的输出模式。您可以从’ order_bys ‘中提取排序键,然后使用’ std::sort '和自定义比较器对子节点的元组进行排序。

/** The sort plan node to be executed */
const SortPlanNode *plan_;

std::unique_ptr<AbstractExecutor> child_executor_;
std::vector<Tuple> result_;  // 保存所有元组,并排序
size_t index_;               // 标记是否全部发射
//SortPlanNode属性
std::vector<std::pair<OrderByType, AbstractExpressionRef>> order_bys_;

核心代码是Init()中使用的std::sort()将子执行器冒上来的全部记录进行排序,其中使用lambda表达式表示比较函数。

std::sort(..., [a = x](n, m){...});
//[a=x]是lambda表达式的捕获列表,用于指定在lambda函数体内部可以访问的外部变量,a是内部变量,x是外部变量
//(n, m)是lambda函数的参数列表,指定了lambda函数可以接受的参数

Next()就是不断地从已排序的result_中获取元组返回。

Limit

’ LimitExecutor '限制了其子执行器输出元组的数量。如果其子执行器生成的元组数量少于计划节点中指定的限制,则此执行器不起作用,并生成它接收到的所有元组。

该计划节点具有与其输入模式相同的输出模式。您不需要支持偏移量。

/** The limit plan node to be executed */
const LimitPlanNode *plan_;

/** The child executor from which tuples are obtained */
std::unique_ptr<AbstractExecutor> child_executor_;

std::size_t sum_;  // 标记已发射多少tuple
auto LimitExecutor::Next(Tuple *tuple, RID *rid) -> bool {
  if (child_executor_->Next(tuple, rid) && sum_ < plan_->GetLimit()) {
    ++sum_;
    return true;
  }
  return false;
}

Top-N优化规则

EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;

默认情况下,BusTub将这个查询计划为’ SortPlanNode ‘和’ LimitPlanNode '。这是低效的,因为可以使用堆来跟踪最小的10个元素,这比对整个表进行排序要有效得多。

使用topN堆优化比单纯执行排序并限制输出数量快的原因是,topN堆优化可以在查询过程中动态地维护一个大小为N的堆,只保留当前最大(或最小)的N个元素,而不需要对整个结果集进行排序。

当执行带有order by和limit关键字的SQL时,单纯执行排序并限制输出数量需要对整个结果集进行排序操作,这个操作的时间复杂度通常是O(n log n),其中n是结果集的大小。而使用topN堆优化可以将排序的操作推迟到最后,只需要在堆中维护N个元素的有序性,时间复杂度通常是O(n log N),其中N是限制的输出数量。

因此,使用topN堆优化可以减少排序的操作次数和排序的数据量,从而提高查询的效率。尤其是当结果集很大时,topN堆优化可以显著减少排序操作的时间消耗,提升查询性能。

Init()子执行器初始化,定义一个比较函数cmp,根据比较函数定义优先队列std::priority_queue<Tuple, std::vector<Tuple>, decltype(cmp)> myqueue(cmp);,这个优先队列经过限制大小可以起到Top N堆优化的作用,优先队列是使用堆实现的,其思想是构建一个容量固定的容器来保持局部有序,超过容量的被抛弃,这样当遍历完所有数据时就能得出容量大小的Top N结果。比较函数中包含了针对ORDER BY的判断,这样就可以一段代码分别应对处理生成大堆还是小堆。之后循环获取子执行器的所有结果添加到优先队列。此时优先队列已将所有元组进行排序,然后根据执行计划的n_(即LIMIT的值)将优先队列的前(top()n_个插入进results_

auto cmp = [order_bys = plan_->order_bys_, schema = child_executor_->GetOutputSchema()](const Tuple &left_tuple, const Tuple &right_tuple) {
    for (const auto &pair : order_bys) {
        // 分别取出列值
        const auto &left_value = pair.second->Evaluate(&left_tuple, schema);
        const auto &right_value = pair.second->Evaluate(&right_tuple, schema);

        if (pair.first == OrderByType::DESC) {  // 降序,使用优先队列时:若是升序则是大顶堆,大的数在前,与sort相反
            if (left_value.CompareLessThan(right_value) == CmpBool::CmpTrue) {
                return true;
            }
            if (left_value.CompareGreaterThan(right_value) == CmpBool::CmpTrue) {
                return false;
            }
        } else {
            if (left_value.CompareGreaterThan(right_value) == CmpBool::CmpTrue) {
                return true;
            }
            if (left_value.CompareLessThan(right_value) == CmpBool::CmpTrue) {
                return false;
            }
        }
        // 若相等,继续比较
    }
    return false;  // 全部反过来
};

Next()results_中获取元组。

优化是在Optimizer::OptimizeSortLimitAsTopN()中实现的,同样是先取出执行计划的子结点进行递归优化,将优化好的孩子列表调用CloneWithChildren()新克隆一个结点,强转为LimitPlanNode取出计划类型是Sort的子计划结点,强转为SortPlanNode,返回TopNPlanNode型的共享指针。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1118100.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

反射、注解、元注解、动态代理

反射 反射就是&#xff1a;加载类&#xff0c;并允许以编程的方式解剖类中的各种成分&#xff08;成员变量、方法、构造器等 学习反射就是学习如何获取类的信息并操作它们 加载类&#xff0c;获取类的字节码&#xff1a;Class对象获取类的构造器&#xff1a;Constructor对象获…

滚珠螺杆应如何存放避免受损

滚珠螺杆是一种高精度的机械零件&#xff0c;保存或使用不当&#xff0c;会直接损坏&#xff0c;影响生产效率&#xff0c;因此我们在使用时需要注意以下事项&#xff1a; 1、避免垂直放置&#xff1a;没有施加预压的螺杆垂直放置时&#xff0c;螺母会因自重而从螺杆轴上脱荐下…

【斗破年番】谣言不攻自破,萧潇造型曝光,制作进度已达中州,风尊者帅翻

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析重要国漫资讯。 斗破苍穹动画中&#xff0c;萧炎与小医仙重聚&#xff0c;也即将与美杜莎女王回蛇人族见家长&#xff0c;剧情一度变得愈加的炸裂&#xff0c;颇有逐鹿鹅厂国漫第一把交椅的架势。正因此&#xff0c;斗破动…

强化学习 | Python强化学习

强化学习在近年来取得了巨大的突破,使机器能够在不断的试错中自动学习并做出决策。 本文将介绍强化学习的基本概念、原理和应用,同时提供详细的公式解释和Python代码示例。 强化学习是什么? 强化学习是一种机器学习方法,用于让智能体(例如机器人、自动驾驶汽车或游戏玩家…

Keil 5 安装教程(最新最全最详细)附网盘资源

一.简介 文章转自其他平台 链接: keil5下载连接. 官方下载地址&#xff1a;https://www.keil.com/download/product/ Keil5&#xff08;32/64&#xff09;位下载地址&#xff1a; 链接&#xff1a; https://pan.baidu.com/s/1Jn15jeb0Aa1cSietvXfcwg 密码&#xff1a;8ji…

基于springboot实现财务管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现财务管理系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#x…

记录阿里云服务器(Centos7.9)部署Thingsboard(3.4.2)遇到的一些问题

记录编译Thingsboard遇到的一些问题 部署了一个thingsboard项目到阿里云服务器上&#xff0c;历时十一天&#xff0c;遇到了很多困难&#xff0c;国内关于Thingsboard的资料确实很少&#xff0c;所以想着写一篇博客记录一下&#xff0c;或许能够给以后编译遇到类似问题的人一些…

Pandas数据处理分析系列3-数据如何预览

Pandas-数据预览 Pandas 导入数据后,我们通常需要对数据进行预览,以便更好的进行数据分析。常见数据预览的方法如下: ①head() 方法 功能:读取数据的前几行,默认显示前5行 语法结构:df.head(行数) df1=pd.read_excel("销售表.xlsx",sheet_name="手机销…

AUTOSAR EcuM休眠阶段的具体实现详解

在AUTOSAR EcuM SWS里对于Sleep阶段做出了一个宏观的流程设计,如下: 从BswM过渡到EcuM的规则仲裁这里暂时不讲,有兴趣可以看之前我遇到的工程问题分析,特别是BswM状态迁移图: BswM状态分析 今天主要聊AUTOSAR规定的sleep两种模式:Halt和Poll,以及这两种模式下,…

【趣味随笔】农业机器人的种类与发展前景

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

在Mac上使用安卓桌面模式

在安装Homeblew的基础上 替换国内源 export HOMEBREW_API_DOMAIN"https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api" export HOMEBREW_BREW_GIT_REMOTE"https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git" brew update 安装Scrcpy …

【2023_10_21_计算机热点知识分享】:机器学习中的神经网络

今天的分享主题是机器学习中的神经网络。神经网络是一种模拟人类神经系统的计算模型&#xff0c;它由一系列的神经元组成&#xff0c;每个神经元接收一组输入&#xff0c;经过计算后产生一个输出。神经网络的学习过程是通过调整神经元之间的连接权重来实现的&#xff0c;这个过…

Unity--用户界面

目录 “使用工具栏”&#xff1a; “层次结构”窗口&#xff1a; 层次结构窗口 制作子GameObject “游戏”视图&#xff1a; “场景视图“&#xff1a; ”项目窗口“&#xff1a; 项目窗口工具栏&#xff1a; "Inspector" 窗口&#xff1a; Inspector 游戏…

UG\NX二次开发 实时查看 NX 日志文件

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 感谢粉丝订阅 感谢 a18037198459 订阅本专栏,非常感谢。 简介 实时查看 NX 日志文件,有助于分析保存时间等。打开WindowsPowerShell并实时获取日志文件内容的小功能。 效果 代…

力扣每日一题52:N皇后问题||

题目描述&#xff1a; n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回 n 皇后问题 不同的解决方案的数量。 示例 1&#xff1a; 输入&#xff1a;n 4 输出&#xff1a;2 解释&#…

Pyside6 QFileDialog

Pyside6 QFileDialog Pyside6 QFileDialog常用函数getOpenFileNamegetOpenFileNamesgetExistingDirectorygetSaveFileName 程序界面程序主程序 Pyside6 QFileDialog提供了一个允许用户选择文件或目录的对话框。关于QFileDialog的使用可以参考下面的文档 https://doc.qt.io/qtfo…

Easyx趣味编程7,鼠标消息读取及音频播放

hello大家好&#xff0c;这里是dark flame master&#xff0c;今天给大家带来Easyx图形库最后一节功能实现的介绍&#xff0c;前边介绍了绘制各种图形及键盘交互&#xff0c;文字&#xff0c;图片等操作&#xff0c;今天就可以使写出的程序更加生动且容易操控。一起学习吧&…

CUDA编程入门系列(十)并行规约

一、什么是规约&#xff1f; 规约&#xff0c;用白话来说&#xff0c;就是将多个值变成一个值的操作&#xff0c;如向量求和&#xff0c;向量内积操作。 以向量求和为例&#xff0c;如果使用串行规约的话&#xff0c;那么就是依靠for循环来进行操作 for(int i 0; i < nums.…

泛微 E-Office download.php 任意文件读取漏洞

一、漏洞描述 泛微E-Office是一款企业级的全流程办公自动化软件&#xff0c;它包括协同办公、文档管理、知识管理、工作流管理等多个模块&#xff0c;涵盖了企业日常工作中的各个环节。泛微E-Office能够帮助企业实现全流程数字化、自动化&#xff0c;提高工作效率和管理质量&a…

#define 宏定义看这一篇文章就够了

前言&#xff1a;在c/c学习的过程中&#xff0c;宏定义&#xff08;#define&#xff09;是作为初学者学习到的为数不多的预处理指令&#xff0c;在学习的时候我们被告知他可以帮助我们更高效的写程序&#xff0c;可以增加程序的可读性&#xff0c;但宏定义&#xff08;#define&…