CMU15445(2023fall) Project #3 - Query Execution(上)详细分析

晚日寒鸦一片愁

柳塘新绿却温柔

若教眼底无离恨

不信人间有白头

——鹧鸪天

完整代码见:

SnowLegend-star/CMU15445-2023fall: Having Conquered the Loftiest Peak, We Stand But a Step Away from Victory in This Stage. With unwavering determination, we press onward, for destiny favors those brave enough to forge ahead, cutting through the thorns and overcoming every obstacle that dares to stand in their way.

 

目录

Project #3 - Query Execution

调试说明

执行计划分析

Task #1 - Access Method Executors

1. 顺序扫描(SeqScan)

2. 插入(Insert)

3. 更新(Update)

4. 删除(Delete)

 5. IndexScanExecutor(Index)

6. 优化顺序扫描为索引扫描(SeqScan → IndexScan)

Task #2 - Aggregation & Join Executors

Aggregation

NestedLoopJoin


Project #3 - Query Execution

调试说明

要准确体会executor的执行过程,进行调试是相当重要的。但是Lab 03的测试方法不同于以前,是利用sqllogictest来执行slt文件。

1. make -j$(nproc) sqllogictest2. ./bin/bustub-sqllogictest ../test/sql/p3.00-primer.slt –verbose

搞得我一直以为调试要从sqllogictest下手,但这样配置显然是有问题的

        去群里问了也没有得到想要的答案。反复看了几遍handout才发现有可能是通过bustub-shell进行调试,试了一下还真是。

        会了如何进行调试之后,我们就可以着手先分析各种sql语句的执行过程了。首先,我们可以先执行几遍sql语句来对Query Processing有个大致的了解,如下图。

执行计划分析

简略分析SELECT colA FROM __mock_table_1;的执行过程

以看到这里的expression是“#0.0”,表示左表(当前表)的第0列元素,对应colA。

显然,colA属于一个column_value_expression

expr->Evaluate(&child_tuple, child_executor_->GetOutputSchema())

对于这句代码,意思是在表__mock_table_1中,提取当前tuple对象的第0列元素。

至于tuple的data部分要怎么看,我们可以结合每个列的类型具体分析。比如这里__mock_table_1的两个column都是int类型的,各占4个字节,加起来就是8字节。

又可以发现tuple的data部分是char *类型的数组,所以数组的前4位都是表示colA的值,数组的后4位表示colB的值。

再进一步分析char *data是怎么表示数值的。我们可以发现100对应“d”,这是因为d的ASCII码恰好是100,故可以用“d”来表示100。

而data[4]=‘,’,data[5]=‘\x01’又是怎么表示300的呢?

在小端字节序中,低位字节(较小的数字)存储在内存的低地址处,较高位字节存储在高地址处。

  • data[0] = ','(0x2C,44 十进制)存储在内存低地址。
  • data[1] = '\x01'(0x01,1 十进制)存储在内存高地址。

那么这两个字节拼接起来就变成了:

0x01 0x2C

即,低字节 0x2C 和高字节 0x01 组合起来形成了一个 16 位的数字。我们可以将它看作是一个十六进制数 0x012C,转化为十进制就是:

0x012C = 1 * 256 + 44 = 300

所以,data[0] = ',' 和 data[1] = '\x01' 会在小端字节序中组合成十进制的 300。如下图

再用filter举一个例子。

以此类推直至遍历结束。

        

然后,第二个中难点就是搞清楚各种expressions的作用了。关于Lab03的各种关键代码的分析,由于内容过多我直接放在github里面了。强烈推荐结合这张图先完全领悟这些关键代码再开始写,不然会异常折磨

上面内容后面还会用到,务必要领悟通透!

下面正式开始分析task。

Task #1 - Access Method Executors

1. 顺序扫描(SeqScan)

着手开始实现时,首先还是要先看看这个语句的执行计划。

        可以看到,SeqScan就是最底层的executor,它没有child_executor的存在。至于为什么“Planner”部分的执行计划可以优化为“Optimizer”部分的呢?这里就是课上提到的“投影下移”——直接在顺序扫描table的时候就提取所需的column(这里是column1、2),而不用扫描完所有tuple后再做一次Project。

        而init()和next()两个函数到底该如何实现,我们可以先从next()的注释入手:next()返回一个生成的tuple。注意到,这里next()一次只能返回一个tuple,所以如果我们想要在next()内直接用for循环的方式遍历table是不可取的,这样难以记录当前访问的tuple的下标。再根据实验说明提到的“iter自增问题”,用迭代器遍历table的想法就应运而生了。每次next()内部获得新的tuple后,迭代器只需要自增1就可以满足记录到底访问到table的什么位置了。至于在哪里初始化这个迭代器,自然而然就可以想到init()了。显然,SeqScan只需要调用一次init(),调用sizeof(table)次next()。

更具体地说,完成SeqScan可以分为两步走:

  1. Init 方法首先获取要扫描的表的堆(table_heap_),然后创建一个迭代器 iter来遍历这个堆。它遍历堆中的所有记录,并将它们的记录标识符(RID)存储在rids_列表中。最后,它将rid_iter_设置为rids_列表的起始位置。
  2. Next方法用于从表中检索下一个满足条件的元组。它使用一个do-while循环来遍历rids_ 列表中的 RID。对于每个 RID,它首先获取该 RID 对应的元组的元数据(TupleMeta)。如果该元组没有被删除(!meta.is_deleted_),则将该元组复制到传入的 tuple参数中,并将 RID 复制到rid参数中。

        然后,它检查是否有过滤条件(plan_->filter_predicate_。如果有,它会评估这个过滤条件,并检查结果是否为 true。只有当元组没有被删除且满足过滤条件时,do-while 循环才会结束。

2. 插入(Insert)

老样子我们先看看insert_executor的具体查询计划

可以看到insert_executor是拥有一个child_exector的,这个child类型是“Value_executor”。没错,就是实验说明三个实例之一的那个Value。其次,从insert_executor的实验说明可以看到它的next()函数输出并不和SeqScan一样,它会输出“一个整数元组,表示成功插入的行数”。那就是说我们直接在next()内部执行这个插入就可以了,insert_executornext()只需要执行一次,将child_executor包含的几个value插入完毕后就可以返回了。

但是,还要注意插入values成功后,还要及时更新索引。有一说一,这里索引出现得有点突兀。举个例子,我们刚才创建的表t1在第一列(v1)有一个可扩展哈希索引,就是Project 2实现的可扩展哈希表。显然,我们需要把表t1所有的tuple都放进可扩展哈希表索引中, v1这列的值进行hash()后就作为索引的key,整个tuple作为索引的value。

所以在插入了新的tuple后,我们很自然地就需要为可扩展哈希表也添加这个新的元素,想办法怎么联系到可扩展哈希表的Insert(const K &key, const V &value, Transaction *transaction)。具体可以看那张总体函数调用流程图。

分享几个我在实现insert时想到的几个问题

Q:如果在进行插入的时候,发现当前table page已经满了,哪里会处理table page++这个操作呢?

A:table_heap内的InsertTuple()会处理。

Q:感觉insert会调用seq_scan知道找到一个空的slot,那应该如何实现这点呢?

A:同样InsertTuple()会处理。

Q:索引的类型是什么呢?

A:稠密索引,即为每个tuple都生成一个索引。其实我们在Project 2实现的可扩展哈希表也是稠密索引,因为我们把每个tuple都插入到哈希表中了。

3. 更新(Update)

        老样子先看update_executor的执行计划,我们发现它和insert_executor的执行计划差不多。它也有一个child_executor,child的类型也是SeqScan。这里如果还是对update这个操作感到疑惑,可以直接去看看测试点15445/test/sql里面的测试语句。自己将这些语句复制到官方提供的shell中跑一遍,通过explain看看执行计划到底是怎么样的。

        Update的next()也是只执行一次——“返回一个整数,表示更新的行数”。在更新tuple的时候,我们要注意更新的方式是“先删除旧的tuple,再插入新的tuple”。至于删除tuple的方式很简单,修改改tuple的TupleMeta即可。对索引的更新同样如此“先在哈希表中删除这个tuple,再向哈希表中插入新的tuple”。

4. 删除(Delete)

        通过观察这个Delete_executor的执行计划,我们还可以发现一点——可以将filter下移与SeqScan结合,直接在顺序扫描table的同时提取符合predicate的元素就可以了。

        Delete_executor的实现和insert、update类似,就是它要更为简洁一些。找到符合predicate的tuple后,将其TupleMeta的is_deleted置为true即可。同时记得删除这个tuple在哈希表中的内容。

 5. IndexScanExecutor(Index)

它来了它来了,期待已久的index终于来了。乍一看还是两眼一黑的状态,这时不妨先结合测试点进行一番debug看看执行过程。

 

        我们可以发现IndexScan也是没有child_executor的,因为它本质是SeqScan的变体。利用索引来快速查找符合filter的tuple,参考利用Project 2的可扩展哈希表进行快速查找。值得一提的是,由于官方提供的shell版本是最新的,所以在官方shell中显示的索引类型是BplusTree。

        要完成这次的task,得多看几遍实验说明了,我大致总结了下:

  • 先用htable_ = dynamic_cast<HashTableIndexForTwoIntegerColumn *>(index_info_->index_.get());来生成索引
  • 然后,你可以使用哈希索引进行点查找,查找满足谓词的元组,并将这些元组逐个发出。BusTub 只支持具有单一整数列的索引。我们的测试用例不会插入重复的键值。因此,点查找只会返回一个元组(如果存在)。
  • 提示:在查询时,我们不会插入具有重复键的行
  • 你还需要确保不会发出已删除的元组。

        在完成索引扫描后,我们会发现执行计划依然没有从SeqScan更正为IndexScan,原因是我们还没有完成优化规则。

6. 优化顺序扫描为索引扫描(SeqScan → IndexScan)

这部分也没什么好讲解的,主要是读懂实验说明部分。

  1. 启用谓词下推到 SeqScan
    • 我们可以在 SeqScanExecutor 中实现谓词过滤,以便稍后索引扫描节点可以得到该谓词。优化器规则 MergeFilterScan 已经在启动时提供,你只需要确保该规则正常工作。
  2. 使用索引
    • 你可以检查谓词中的过滤列。如果该列上存在索引,那么创建 IndexScanPlanNode。要获得满分,你只需要支持针对索引列上的单一相等谓词的优化(例如:WHERE v1 = 1)。查询 SELECT * FROM t1 WHERE v1 = 1 AND v2 = 2 应该仍然使用 SeqScan,因此你不需要拆分谓词。

        而且光分析起来就显得罗里吧嗦,直接上代码。

auto Optimizer::OptimizeSeqScanAsIndexScan(const bustub::AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {// TODO(student): implement seq scan with predicate -> index scan optimizer rule// The Filter Predicate Pushdown has been enabled for you in optimizer.cpp when forcing starter rulestd::vector<AbstractPlanNodeRef> children;for (const auto &child : plan->GetChildren()) {children.emplace_back(OptimizeSeqScanAsIndexScan(child));}auto optimized_plan = plan->CloneWithChildren(std::move(children));// 尝试将seqScan转化为indexScanif (optimized_plan->GetType() == PlanType::SeqScan) {// 将当前plan转化为seq_planconst auto &seq_plan = dynamic_cast<const SeqScanPlanNode &>(*optimized_plan);//如果seq没有filter_predicate, 那就是将整个table传上去, 也没必要用到索引if (seq_plan.filter_predicate_ != nullptr) {auto table_info = catalog_.GetTable(seq_plan.table_oid_);auto table_indexes = catalog_.GetTableIndexes(seq_plan.table_name_);// 如果filter_pre是一个逻辑谓词 例如v1 = 1 AND v2 = 2, 则直接返回, 因为bustub的索引都是单列的, 两者不可能匹配if (auto tmp = dynamic_cast<LogicExpression *>(seq_plan.filter_predicate_.get());tmp != nullptr && table_indexes.empty()) {return optimized_plan;}// 这个filter_pri实际上是一个compExpr 例如where v1=1auto filter_comp_expr = dynamic_cast<ComparisonExpression *>(seq_plan.filter_predicate_.get());if (filter_comp_expr != nullptr && filter_comp_expr->comp_type_ == ComparisonType::Equal) {// 提取filter_pre中的列名auto filter_colval_expr = dynamic_cast<ColumnValueExpression *>(filter_comp_expr->children_[0].get());if (filter_colval_expr == nullptr) {  // 其实到这一步基本不可能出问题了return optimized_plan;}std::vector<uint32_t> filter_colval_expr_ids{filter_colval_expr->GetColIdx()};  // 装模作样把一个元素塞进vector里// 开始判断filter_pre和索引的关系for (const auto &index : table_indexes) {auto index_columns = index->index_->GetKeyAttrs();if (index_columns == filter_colval_expr_ids) {auto pred_key = dynamic_cast<ConstantValueExpression *>(filter_comp_expr->children_[1].get());return std::make_shared<IndexScanPlanNode>(optimized_plan->output_schema_, table_info->oid_,index->index_oid_, seq_plan.filter_predicate_, pred_key);}}}}}return optimized_plan;
}

        需要注意的一点是,这次需要设置next(tuple,rid)中的tuple和rid,不能向前几个一样只是设置了tuple而没有管rid,不然就会报错。

        有一说一这花花绿绿的配色还挺好看,嘻嘻~

Task #2 - Aggregation & Join Executors

Aggregation

        我个人感觉要完成这个task,重中之重是理解那几个expression类。在充分理解了expression后,就可以着手对aggregation_executor进行分析了。

        先看aggregation_executor的执行计划,我们需要重点关注的就是types(聚合操作的类型)aggregates(聚合操作针对的列)group by(基于哪一列进行分组)。显然,这次我们还是需要用到column_expr.evaluate()来提取某个tuple中对应的列值。

        正如实验说明里提到的,aggregations操作是pipeline breakers,即我们不能一边从child_executor中获得tuple,一边对这些tuple进行聚合操作。相反,聚合函数需要先从其子节点获取所有相关的数据,完成整个聚合计算过程(例如计算总和、平均值、最小/最大值等),然后才能产生输出结果。综上,我们可以将遍历child_executor和聚合操作都放在init()中实现。在next()进行对聚合哈希表的遍历即可

        对于哈希表的使用,可以归纳为以下几个步骤:

  1. 在child_executor中提取一个tuple
  2. 在tuple中,提取group by(col_expr类型)对应的列组成values1,用values1构造一个AggregateKey,即哈希表的key,这也正是MakeAggregateKey()的作用。注意,group by可能由不止一个attribute组成。
  3. 在tuple中,提取aggregates(col_expr类型)对应的列组成values2,用values2构造一个AggregateValue,即哈希表的value,这是正是MakeAggregateValue()的作用。Aggregates的个数和聚合函数的个数是一一对应的。
  4. 将{AggregateKey,AggregateValue}插入到哈希表中进行聚合运算,并将结果存储起来。

        这个task的重难点就是完成哈希表的操作,理解了上面那个过程就可以较为轻松完成了。最后,还会遇到一个比较奇怪的测试。如果这个table是空的,但是聚合函数只有count(*),最终结果还是要输出0,而不是什么都不输出。但是,在table为空的情况下,如果聚合函数有count(*)和max(cloA)等其他聚合函数,那就是输出空值。

NestedLoopJoin

        像这里的predicate本质是一个comparison_expression,它有两个child。Child[0]是一个col_expr(col0),child[1]也是个col_expr(col2)。

        这个task感觉没啥好说的,就是对各种expression的运用,一开始已经分析过了,就不再赘述。实验说明里面的一句话说的就是这个意思。

提示:你应该使用 NestedLoopJoinPlanNode 中的谓词。参考 AbstractExpression::EvaluateJoin,它处理左侧元组和右侧元组及其各自的模式。此方法返回一个 Value,它可以是 falsetrue NULL。你可以参考 FilterExecutor,了解如何在元组上应用谓词。

        我们需要注意的一点是,存在这样一种情况:左表的某个tuple,在右表有多个匹配的tuple,就像下面这种错误

        这里讲讲什么时候用自身的OutputSchema()、什么时候用child-> OutputSchema()吧。

        一般来说,在next()中用values来构造输出的tuple时需要用到自身的output_schema;当我们用相关的expr提取tuple的某一列值时,用到的就是child的output_schema。因为像predicate、group by等等列col_Expr集合,他们都是作用于从child_executor中提取出来的tuple,所以这个tuple的初始类型就是child的output_schema。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【网络安全 | 漏洞挖掘】利用文件上传功能的 IDOR 和 XSS 劫持会话

未经许可,不得转载。 本文涉及漏洞均已修复。 文章目录 前言正文前言 想象这样一个场景:一个专门处理敏感文档的平台,如保险理赔或身份验证系统,却因一个设计疏漏而成为攻击者的“金矿”。在对某个保险门户的文件上传功能进行测试时,我意外发现了一个可导致大规模账户接管…

知识图谱-资源网

知识图谱-资源网 http://openkg.cn/datasets-type/https://www.ownthink.com/knowledge.html

【湖北省计算机信息系统集成协会主办,多高校支持 | ACM出版,EI检索,往届已见刊检索】第二届边缘计算与并行、分布式计算国际学术会议(ECPDC 2025)

第二届边缘计算与并行、分布式计算国际学术会议&#xff08;ECPDC 2025&#xff09;将于2025年4月11日至13日在中国武汉盛大召开。本次会议旨在为边缘计算、并行计算及分布式计算领域的研究人员、学者和行业专家提供一个高水平的学术交流平台。 随着物联网、云计算和大数据技术…

【Qt】MVC设计模式

目录 一、搭建MVC框架 二、创建数据库连接单例类SingleDB 三、数据库业务操作类model设计 四、control层&#xff0c;关于model管理类设计 五、view层即为窗口UI类 一、搭建MVC框架 里面的bin、lib、database文件夹以及sqlite3.h与工程后缀为.pro文件的配置与上次发的文章…

Grok3使用体验与模型版本对比分析

文章目录 Grok的功能DeepSearch思考功能绘画功能Grok 3的独特功能 Grok 3的版本和特点与其他AI模型的比较 最新新闻&#xff1a;Grok3被誉为“地球上最聪明的AI” 最近&#xff0c;xAI公司正式发布了Grok3&#xff0c;并宣称其在多项基准测试中展现了惊艳的表现。据官方消息&am…

Pytest测试用例执行跳过的3种方式

文章目录 1.前言2.使用 pytest.mark.skip 标记无条件跳过3.使用 pytest.mark.skipif 标记根据条件跳过4. 执行pytest.skip()方法跳过测试用例 1.前言 在实际场景中&#xff0c;我们可能某条测试用例没写完&#xff0c;代码执行时会报错&#xff0c;或者是在一些条件下不让某些…

DeepSeek开源周Day5: 3FS存储系统与AI数据处理新标杆

项目地址&#xff1a; GitHub - deepseek-ai/3FS: A high-performance distributed file system designed to address the challenges of AI training and inference workloads.GitHub - deepseek-ai/smallpond: A lightweight data processing framework built on DuckDB and…

什么是多线程?线程池?

文章目录 一、什么是多线程&#xff1f;二、多线程的实现方法1. 继承Thread类,重写run方法2. 实现Runnable接口&#xff0c;并创建Thread对象3. Callable和Future 三、线程的5种状态**New&#xff08;新创建&#xff09;****Runnalbe(可运行)****Running****Blocked(阻塞)****等…

MES生产制造执行管理系统(源码+配套文档)

在当今竞争激烈的制造业环境中&#xff0c;企业要想保持竞争优势&#xff0c;就必须不断提升生产效率、优化管理流程。MES&#xff08;制造执行系统&#xff09;作为连接上层计划管理与底层工业控制的桥梁&#xff0c;正逐渐成为众多制造企业转型升级的关键工具。一个功能全面的…

AI伦理挑战:如何确保技术发展符合道德规范?

引言 随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;我们正迎来一个前所未有的数字化时代。AI的应用已经渗透到医疗、教育、金融、交通等众多领域&#xff0c;极大地推动了生产效率的提升&#xff0c;改善了人们的生活质量。从智能医疗诊断到自动驾驶汽车…

Qt 自带颜色属性

Qt 系统自带颜色如下&#xff1a; enum GlobalColor {color0,color1,black,white,darkGray,gray,lightGray,red,green,blue,cyan,magenta,yellow,darkRed,darkGreen,darkBlue,darkCyan,darkMagenta,darkYellow,transparent};对应颜色如下&#xff1a; color0: 这是自定义颜色…

MySQL慢查询分析与处理

什么是慢日志 慢日志是MySQL用来记录数据库中执行较慢的SQL语句的日志&#xff0c;当数据库遇到性能问题时&#xff0c;慢日志可以帮助我们分析数据库中执行较慢的SQL。 如何打开数据库慢日志功能 MySQL默认是关闭慢日志功能的&#xff0c;可以从数据库中或者从配置文件中进行…

深度学习基础--ResNet50V2网络的讲解,ResNet50V2的复现(pytorch)以及用复现的ResNet50做鸟类图像分类

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 如果说最经典的神经网络&#xff0c;ResNet肯定是一个&#xff0c;从ResNet发布后&#xff0c;作者又进行修改&#xff0c;命名为ResNe50v2&#xff0c…

TikTok隐私保护措施:确保用户安全

TikTok隐私保护措施&#xff1a;确保用户安全 在这个信息爆炸的时代&#xff0c;社交媒体平台的隐私保护问题日益成为公众关注的焦点。TikTok&#xff0c;作为全球领先的短视频平台&#xff0c;拥有庞大的用户群体&#xff0c;因此&#xff0c;其隐私保护措施显得尤为重要。本…

FFmpeg-chapter3-读取视频流(原理篇)

ffmpeg网站&#xff1a;About FFmpeg 1 库介绍 &#xff08;1&#xff09;libavutil是一个包含简化编程函数的库&#xff0c;包括随机数生成器、数据结构、数学例程、核心多媒体实用程序等等。 &#xff08;2&#xff09;libavcodec是一个包含音频/视频编解码器的解码器和编…

【Redis】Mac系统一键安装redis

要在 macOS 上一键安装 Redis&#xff0c;可以使用 Homebrew&#xff08;一个流行的包管理工具&#xff09;来简化安装过程。下面是可以执行的安装脚本&#xff1a; 安装脚本&#xff1a; #!/bin/bash# 检查 Homebrew 是否已安装&#xff0c;如果没有安装&#xff0c;则安装 …

P1149 [NOIP 2008 提高组] 火柴棒等式c/c++

P1149 [NOIP 2008 提高组] 火柴棒等式c/c 题目描述 给你 n 根火柴棍&#xff0c;你可以拼出多少个形如 ABC 的等式&#xff1f;等式中的 A、B、C 是用火柴棍拼出的整数&#xff08;若该数非零&#xff0c;则最高位不能是 0&#xff09;。用火柴棍拼数字 0∼9 的拼法如图所示&a…

七星棋牌 6 端 200 子游戏全开源修复版源码(乐豆 + 防沉迷 + 比赛场 + 控制)

七星棋牌源码 是一款运营级的棋牌产品&#xff0c;覆盖 湖南、湖北、山西、江苏、贵州 等 6 大省区&#xff0c;支持 安卓、iOS 双端&#xff0c;并且 全开源。这个版本是 修复优化后的二开版本&#xff0c;新增了 乐豆系统、比赛场模式、防沉迷机制、AI 智能控制 等功能&#…

安全模块设计:token服务、校验注解(开启token校验、开启签名校验、允许处理API日志)、获取当前用户信息的辅助类

文章目录 引言pom.xmlI 校验注解ApiValidationII token服务TokenService获取当前用户信息的辅助类III 域登录接口响应数据登陆用户信息引言 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/PO…

贪心算法精品题

1.找钱问题 本题的贪心策略在于我们希望就可能的保留作用大的5元 class Solution { public:bool lemonadeChange(vector<int>& bills) {std::map<int ,int> _map;for(auto ch:bills){if(ch 5) _map[ch];else if(ch 10){if(_map[5] 0) return false;else{_m…