曾经沧海难为水,除却巫山不是云。|

Joey-Wang

园龄:4年2个月粉丝:17关注:0

2023-04-10 00:28阅读: 597评论: 0推荐: 0

Project #3 - Query Execution 项目要求

https://15445.courses.cs.cmu.edu/fall2022/project3/

Project #1 中我们实现了一个 buffer pool manager。Project #2 中我们实现了一个 B+Tree 索引。在此次 Project,你将实现一个让 BusTub 执行 query 的组件。你要创建 operator executors 来执行 SQL queries,实现 optimizer rules 来 transform query plans。

BACKGROUND

我们首先讨论 query process 的基本知识。请仔细阅读这部分,因为在本项目中,需要你们自己构建 sql queries,以测试 executor 的实现。

BusTub 架构:

image-20230409210940627

我们已经提供了一个完成的 query processing layer,你可以使用 BusTub shell 来执行 SQL queries,就像 homework 1 中的一样。使用如下命令可以编译并构建 BusTub shell。也可以使用 BusTub Web Shell 在线运行 sql queries。

cd build && make -j$(nproc) shell
./bin/bustub-shell

在 shell 中可使用 \dt 获取所有表。默认情况下,BusTub shell 会自动创建三个预先填充了数据的表格。这是为你提供的便利,这样你就不必在每次重建你的代码时加载假数据。当你重新启动 DBMS 时,对这些表的任何改变都不会被保存。

bustub> \dt
+-----+----------------+------------------------------+
| oid | name           | cols                         |
+-----+----------------+------------------------------+
| 0   | __mock_table_1 | (colA:INTEGER, colB:INTEGER) |
| 1   | __mock_table_2 | (colC:VARCHAR, colD:VARCHAR) |
| 2   | __mock_table_3 | (colE:INTEGER, colF:VARCHAR) |
| ... | ...            | ...                          |
+-----+----------------+------------------------------+

你可以通过 SELECT 语句来获取表中的数据。

bustub> SELECT * FROM __mock_table_1;
+---------------------+---------------------+
| __mock_table_1.colA | __mock_table_1.colB |
+---------------------+---------------------+
| 0                   | 0                   |
| 1                   | 100                 |
| 2                   | 200                 |
| 3                   | 300                 |
| 4                   | 400                 |
| 5                   | 500                 |
| ...                 | ...                 |
+---------------------+---------------------+

注意

  • BusTub只支持一小部分的 SQL 语法。如果它对某些 SQL 查询不起作用,请不要感到惊讶。关于BusTub支持的所有 SQL 查询,请参考 tests/sql 中的SQLLogicTest 文件。
  • 如果你使用 CLion 来运行 BusTub 的 shell,请在 shell 中添加一个 --disable-tty 参数,这样它就能在 CLion 终端中正确工作。
  • 语句的结尾一定要用;(内部命令除外)。
  • BusTub只支持 INTVARCHAR(n) 类型。另外,你应该对字符串使用单引号,例如:INSERT INTO table VALUES ('a')

Explain SQL Queries

我们接下来讨论 BusTub shell 怎样知道给出输入 SELECT * FROM __mock_table_1; 时,应该从 __mock_table_1 中读取所有数据。BusTub 也支持 EXPLAIN 语句 print query 的 execution plan。你可以添加 EXPLAIN 在 query 开头,来查看到这些信息:

bustub> EXPLAIN SELECT * FROM __mock_table_1;
=== BINDER ===
BoundSelect {
  table=BoundBaseTableRef { table=__mock_table_1, oid=0 },
  columns=[__mock_table_1.colA, __mock_table_1.colB],
  groupBy=[],
  having=,
  where=,
  limit=,
  offset=,
  order_by=[],
  is_distinct=false,
}
=== PLANNER ===
Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
=== OPTIMIZER ===
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)

EXPLAIN 的结果提供了 query processing layer 内 transformation process 的概况。

语句首先会进入 parser 和 binder,产生 AST 抽象语法树。这让 BusTub 理解 query 想要做什么。在这个例子中,query 希望对 __mock_table_1 执行 BoundSelect,并检索两列 (即 colAcolB)。注意 binder 会自动将原始 SQL query 中的 * 字符扩展为表中的实际列。

然后 binder AST 会进入 planner,planner 会产生 query 的 query plan。query 被 plan 为一棵有两个节点的树,数据从树的叶子流向根。

image-20230409213825244

最后 optimizer 将优化这个 query plan。在这个例子中,他将删除 projection plan node 因为它是冗余的。

下面,来看另一个更复杂的例子:

bustub> EXPLAIN (o) SELECT colA, MAX(colB) FROM
  (SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE) GROUP BY colA;
=== OPTIMIZER ===
Agg { types=[max], aggregates=[#0.1], group_by=[#0.0] }
  NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) }
    MockScan { table=__mock_table_1 }
    MockScan { table=__mock_table_3 }

在 BusTub 中 optimizer 的输出总是一棵树,对于👆这个例子,这棵树长这样:

image-20230409214308915

如果你在 BusTub Web Shell 中运行这个用例,你会发现使用了 HashJoin 而不是 NestedLoopJoin

在本学期的项目中,你无需实现 HashJoin

此次 Project 中,你需要构造 SQL queries 来测试你实现的 executors。EXPLAIN 对于你了解一个 SQL query 是否使用了一个特定的 executor 是非常重要的。

Sample Executors

我们已经实现了几个 executor。下面我们看下 planner 何时会将 SQL queries 计划 (plan) 为这些 plan nodes。

Projection

一个 projection plan node 用于对一个输入进行计算。它总是正好有一个 child 结点。🌰

EXPLAIN SELECT 1 + 2;
EXPLAIN SELECT colA FROM __mock_table_1;
EXPLAIN SELECT colA + colB AS a, 1 + 2 AS b FROM __mock_table_1;

一个 projection plan node 包含几个计算的表达式,它可以是 ColumnValueExpression(此处 colA 在 explain 的时候会被展示为 #0.0),表示直接将 child executor 的第一列放到这个输出列;或者是 ConstantExpression,表示一个常量值 (如 1)。表达式也可以用树形表示,如 1+2 是一个有两个 ConstantExpression (12) 作为孩子结点的 ArithmeticExpression

注意,语法 #0.0 意味着第一个 child 的第一个列, #0.1 意味着第一个 child 的第二个列。在 plan join 的时候,你会看到类似 #0.0 = #1.0 的情况。

Filter

一个 filter plan node 用于使用一个给定的 predicate 过滤 child 的输出。🌰

EXPLAIN SELECT * FROM __mock_table_1 WHERE colA > 1;

一个 filter plan node 正好有一个 child 结点,并包含一个谓词。

Values

一个 values plan node 用于直接产生一个值。🌰

EXPLAIN values (1, 2, 'a'), (3, 4, 'b');
CREATE TABLE table1(v1 INT, v2 INT, v3 VARCHAR(128));
EXPLAIN INSERT INTO table1 VALUES (1, 2, 'a'), (3, 4, 'b');

value plan node 在往 table 中插入用户提供的 value 时非常有用。

Schema

你可能注意到了,在 explain 的时候,每个 plan node 后面都跟着一长串的列表述。这是 plan node 的 expected output schema

Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)

这表明这个 plan node 的 executor 将产生两个列。它们都是整数类型。output schema 是在 planner 上推断出来的。此次 Project 中,你实现的 executor 必须产生与 plan node 中指定的 schema 完全一致的 tuple,否则将无法通过检查输出的测试。

PROJECT SPECIFICATION

对于这个 Project,您需要在 BusTub 中实现额外的 operator executor。我们将使用 iterator query processing model (即 the Volcano model)。在这个模型中,每个 query plan executor 都实现 Next 函数。当 DBMS 调用 executor 的 Next 函数时,executor 返回 (1) a single tuple (2) 一个 no more tuple 的指示符。使用这种方法,每个 executor 实现一个循环,该循环继续对其 children 调用 Next 以检索元组并逐个处理它们。

在 BusTub 的 iterator model 实现中,每个执行器的 Next 函数除了返回一个 tuple 外还返回一个 record 标识符 (RID)。record 标识符用作 tuple 的唯一标识符。

executor 是根据 executor_factory.cpp 中的 execution plan 创建的。

这个 Project 中的所有测试用例都是以一种称为 SQLLogicTest (源自 SQLite) 的特殊文件格式编写的。您可以在本页末尾找到如何使用它。

Task #1 - Access Method Executors

在 background 章节,我们可以看到 BusTub 已经能够在 SELECT 查询中从模拟表中检索数据。这是因为这些是特殊的表,实际上并不存储真正的 tuples。它们是“虚拟”表,是 MockScan Executor 使用预定义的算法生成的始终相同的 tuples。这也是不能更新这些表的原因。

本任务中,你要实现 Executor 来对存储中的表进行读取和写入。

涉及文件:

src/include/execution/seq_scan_executor.h
src/execution/seq_scan_executor.cpp
src/include/execution/insert_executor.h
src/execution/insert_executor.cpp
src/include/execution/delete_executor.h
src/execution/delete_executor.cpp
src/include/execution/index_scan_executor.h
src/execution/index_scan_executor.cpp

SeqScan

The SeqScanPlanNode can be planned with a SELECT * from table statement.

bustub> CREATE TABLE t1(v1 INT, v2 VARCHAR(100));
Table created with id = 15
bustub> EXPLAIN (o,s) SELECT * FROM t1;
=== OPTIMIZER ===
SeqScan { table=t1 } | (t1.v1:INTEGER, t1.v2:VARCHAR)

SeqScanExecutor 遍历一个表并返回它的 tuples,一次一个。

提示:使用 TableIterator 对象时要小心。确保您了解预增量运算符和后增量运算符之间的区别。如果在 ++iteriter++ 之间切换,可能会发现得到了奇怪的输出。

提示:sequential scan 的输出是每个匹配的 tuple 以及它的 record 标识符 (RID)。

注意:BusTub 目前不支持 DROP TABLEDROP INDEX。若要重置数据库则需重新启动 shell。

Insert

The InsertPlanNode can be planned with a INSERT statement.

注意:需要使用单引号来指定 VARCHAR 值。

bustub> EXPLAIN (o,s) INSERT INTO t1 VALUES (1, 'a'), (2, 'b');
=== OPTIMIZER ===
Insert { table_oid=15 } | (__bustub_internal.insert_rows:INTEGER)
  Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:VARCHAR)

InsertExecutor 将 tuples 插入表并更新索引。它正好有一个 child 来生成要插入到表中的值。planner 将确保该值与表具有相同的 schema。executor 将生成一个整数 tuple 作为输出,表示在插入完成后,表中插入的行数。如果表有相关联的索引,请在插入表时更新索引。

提示:在 executor 初始化期间,你需要查找要插入表的表信息。See "System Catalog"。

提示:你需要更新与插入表相关的所有索引。See "Index Updates"。

提示:你需要使用 TableHeap 类来执行表的修改。

Delete

The DeletePlanNode can be planned with a DELETE statement.

bustub> EXPLAIN (o,s) DELETE FROM t1;
=== OPTIMIZER ===
Delete { table_oid=15 } | (__bustub_internal.delete_rows:INTEGER)
  Filter { predicate=true } | (t1.v1:INTEGER, t1.v2:VARCHAR)
    SeqScan { table=t1 } | (t1.v1:INTEGER, t1.v2:VARCHAR)

bustub> EXPLAIN (o,s) DELETE FROM t1 where v1 = 1;
=== OPTIMIZER ===
Delete { table_oid=15 } | (__bustub_internal.delete_rows:INTEGER)
  Filter { predicate=#0.0=1 } | (t1.v1:INTEGER, t1.v2:VARCHAR)
    SeqScan { table=t1 } | (t1.v1:INTEGER, t1.v2:VARCHAR)

DeleteExecutor 删除表的 tuples 并更新索引。它只有一个 child,其中包含了要从表中删除的记录。executor 将生成一个整数输出,表示它从表中删除的行数。还需更新表相关联的索引。

你可以假设 DeleteExecutor 始终位于 query plan 的 root 位置。DeleteExecutor 不应该修改它的结果集。

提示:你需要从 child executor 中获取 RID,并调用 TableHeap::MarkDelete() 来删除 tuple。所有删除都将在事务提交时应用。

提示:你需要更新与删除表相关的所有索引。See "Index Updates"。

IndexScan

IndexScanExecutor 遍历索引来检索 tuple 的 RID,然后 operator 使用这些 RID 在相应的表中检索其 tuple。一次给一个 tuple。

你可通过 SELECT FROM <table> ORDER BY <index column> 来测试你的 index scan executor。下面解释下为什么 ORDER BY 会在 Task #3 中转为 IndexScan

bustub> CREATE TABLE t2(v3 int, v4 int);
Table created with id = 16

bustub> CREATE INDEX t2v3 ON t2(v3);
Index created with id = 0

bustub> EXPLAIN (o,s) SELECT * FROM t2 ORDER BY v3;
=== OPTIMIZER ===
IndexScan { index_oid=0 } | (t2.v3:INTEGER, t2.v4:INTEGER)

本次 Project 中,plan 中的索引对象将始终是 BPlusTreeIndexForOneIntegerColumn。你可以安全地将其强制转换并存储在 executor 对象中:

tree_ = dynamic_cast<BPlusTreeIndexForOneIntegerColumn *>(index_info_->index_.get())

然后你可以从索引对象构造索引迭代器,扫描所有 key 和 tuple IDs,从 table heap 中查找元组。并按索引 key 的顺序发出所有 tuple 作为 executor 的输出。BusTub 仅支持具有单个唯一整数列的索引。测试用例中不会有重复的 key。

提示:现在你已经实现了所有与存储相关的 executor。在下面的任务中,你可以自己创建表并插入一些值来测试你的 executor 执行是否正确。此外还应该通过本地测试 SQLLogicTests #1 to #5

Task #2 - Aggregation & Join Executors

涉及文件:

src/include/execution/aggregation_executor.h
src/execution/aggregation_executor.cpp
src/include/execution/nested_loop_join_executor.h
src/execution/nested_loop_join_executor.cpp
src/include/execution/nested_index_join_executor.h
src/execution/nested_index_join_executor.cpp

Aggregation

AggregationPlanNode 被用来为下面这样的查询提供支持:

EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
EXPLAIN SELECT COUNT(colA), min(colB) FROM __mock_table_1;
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;

注意,aggregation executor 本身不需要处理“having”这个谓词,planner 将 having 作为一个 FilterPlanNode。Aggregation executor 只需要为每组输入做聚合。它有一个 child。

聚合的 schema 是 aggregation 列,然后是 group-by 列。

正如 Lecture 10 中所讨论的,实现聚合的一个常见策略是使用一个哈希表。这就是你在这个 Project 中要使用的方法,然而,我们做了一个简化的假设,即聚合哈希表完全适合在内存中。这意味着你不需要担心实现哈希聚合的两阶段 (Partition, Rehash) 策略。你也可以假设所有的聚合结果都可以驻留在内存中的哈希表中 (也就是说,哈希表不需要由缓冲池页来支持)。

我们提供 SimpleAggregationHashTable 数据结构,它有一个内存哈希表(std::unordered_map) 和一个为计算聚合而设计的接口,还有一个可用于迭代哈希表的 SimpleAggreationHashTable::Iterator 类型。你需要实现 CombinAgregateValues 函数。其中 AggregateValue 是一个 Value 数组,即 input.aggregates_[i]Value 类型。需要注意的是 count(column)count(*) 的区别,以及对空值的处理。

提示:回顾一下,在 query plan 的上下文中,聚合是 pipeline breakers。这可能会影响你在实现中使用 AggregationExecutor::Init()AggregationExecutor::Next() 函数的方式。特别是,要考虑聚合的构建阶段是否应该在 AggregationExecutor::Init()AggregationExecutor::Next() 中执行。

提示:你必须考虑如何处理聚合函数输入中的 NULL (即一个元组可能对聚合函数中使用的属性有一个 NULL 值)。关于预期行为,请参见测试案例。group-by 列将永远不会是 NULL。

提示:当在一个空表上执行聚合时,CountStarAggregate 应该返回 0,所有其他聚合类型应该返回 integer_null。这就是为什么 GenerateInitialAggregateValue() 将大多数聚合值初始化为 NULL。

NestedLoopJoin

DBMS将默认使用 NestedLoopJoinPlanNode 进行所有的 join 操作。example queries:

EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;

你需要通过使用 lecture 中提到的 Simple Nested Loop Join 算法,为 NestedLoopJoinExecutor 实现 inner join 和 left join。这个操作符的 output schema 是左表的所有列,然后是右表的所有列。

这个 executor 应该实现 Lecture 11 中介绍的 Simple Nested Loop Join 算法。也就是说,对于 join 的外表中的每个 tuple,你应该考虑联接内表中的每个 tuple,如果联接谓词得到满足,就发出一个输出 tuple。

提示:你要利用 NestedLoopJoinPlanNode 中的谓词。特别是,看看 AbstractExpression::EvaluateJoin,它处理 left tuple 和 right tuple 以及它们各自的 schema。请注意,这将返回一个 Value,它可能是 false、true 或 NULL。关于如何在元组上应用谓词,请参阅 FilterExecutor

NestedIndexJoin

如果查询包含一个等于条件的 join,并且 join 的右表有一个关于该条件的索引,DBMS将使用 NestedIndexJoinPlanNode。example query:

CREATE TABLE t1(v1 int, v2 int);
CREATE TABLE t2(v3 int, v4 int);
CREATE INDEX t2v3 on t2(v3);
EXPLAIN SELECT * FROM t1 INNER JOIN t2 ON v1 = v3;
=== PLANNER ===
Projection { exprs=[#0.0, #0.1, #0.2, #0.3] } | (t1.v1:INTEGER, t1.v2:INTEGER, t2.v3:INTEGER, t2.v4:INTEGER)
  NestedLoopJoin { predicate=#0.0=#1.0 } | (t1.v1:INTEGER, t1.v2:INTEGER, t2.v3:INTEGER, t2.v4:INTEGER)
    SeqScan { table=t1 } | (t1.v1:INTEGER, t1.v2:INTEGER)
    SeqScan { table=t2 } | (t2.v3:INTEGER, t2.v4:INTEGER)
=== OPTIMIZER ===
NestedIndexJoin { key_predicate=#0.0, index=t2v3, index_table=t2 }
  SeqScan { table=t1 }

在计划阶段, the query is planned as a NestedLoopJoin of two tables. optimizer 识别出 join 的右侧 (SeqScan t2) 在列 v3 上有一个索引,并且连接条件是一个等于条件 v1 = v3。这意味着对于来自左侧的所有 tuples,系统可以使用 key v1 来查询索引 t2v3 以产生连接结果。

NestedIndexJoin 的 schema 是所有来自 left 表 (child, outer) 的列,然后是来自 right 表 (index, inner) 的列。这个 executor 将只有一个 child 来传播对应于 join 的外表的 tuple。对于这些 tuple 中的每一个,你需要通过 catalog 中的索引匹配给定的索引 key,在内表中找到对应的 tuple。

提示:你要从外表获取 tuple,通过使用 key_predicate 构建 index probe key,然后在索引中查找 RID 以检索内表的相应 tuple。

注意:我们永远不会在有索引的表中插入重复的行。

注意:我们将按原样提供 Gradescope 上的所有测试案例。你只需要通过测试。不要考虑 NULL 的奇怪边界情况 (例如,在 group by 和 indices 中的 NULL)。

提示:此时,你应该通过本地测试 SQLLogicTests - #6 to #12

Task #3 - Sort + Limit Executors and Top-N Optimization

涉及文件:

src/include/execution/sort_executor.h
src/execution/sort_executor.cpp
src/include/execution/limit_executor.h
src/execution/limit_executor.cpp
src/include/execution/topn_executor.h
src/execution/topn_executor.cpp
src/optimizer/sort_limit_as_topn.cpp

在开始这个任务之前,你需要先实现 task1 中的 IndexScanExecutor。如果一个表上有一个索引,查询处理层会自动选择用它进行排序。其他情况下,你将需要一个专门的 sort executor 来完成这个任务。

对于所有的要排序的情况,我们假设每个排序键只出现一次,因此你不需要担心排序中的并列问题。

Sort

除了 ORDER BY 属性与索引的 key 相匹配的情况,BusTub 将为所有 ORDER BY 操作符使用一个 SortPlanNode

EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA ASC, colB DESC;

这个 plan node 不改变 schema (也就是说,output schema 与 input schema 相同)。你可以从 ORDER BY 中提取排序键,然后使用 std::sort 并自定义 comparator 来对 child executor 的所有 tuples 进行排序。你可以假设一个表中的所有 entries 都能载入内存。

如果查询在 ORDER BY 子句中不包括排序方向 (即 ASC, DESC),那么排序模式将为 DEFAULT (也就是ASC 升序)。

Limit

LimitPlanNode 指定了查询将产生的 tuple 的数量。

EXPLAIN SELECT * FROM __mock_table_1 LIMIT 10;

LimitExecutor 限制了其 child executor 输出的 tuple 的数量。如果其 child executor 产生的 tuple 数量少于 plan node 中指定的限制,则该 executor 没有任何作用,并输出它所收到的所有 tuples。

这个 plan node 不改变 schema (即,output schema 与 input schema 相同)。你不需要支持 offsets。

Top-N Optimization Rule

对于这最后一项任务,你要修改 BusTub 的 Optimizer 以支持转换 top-N 查询。考虑以下 query:

EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;

默认情况下,BusTub 将通过以下方式执行该查询:(1) 对表中的所有数据进行排序; (2) 获得前10个元素。这显然是低效的,因为该查询只需要最小的几个值。一个更聪明的方法是动态地跟踪到目前为止最小的 10 个元素。这就是 BusTub 的TopNExecutor 所做的。

你将需要修改优化器,以支持将带有 ORDER BY + LIMIT 子句的查询转换为使用 TopNExecutor。参见OptimizeSortLimitAsTopN 以获得更多信息。

上述 query 的 optimized plan:

 TopN { n=10, order_bys=[(Default, #0.0)]} | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
   MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)

注意:想一想可以用什么数据结构来跟踪前 n 个元素 (Andy在 Lecture 中提到过)。这个结构应该最多容纳 k 个元素 (其中 kLIMIT 子句中指定的数字)。

注意:虽然我们没有明确地说,BusTub optimizer 是一个基于规则的优化器。大多数 optimizer rules 以自下而上的方式构建优化计划。

提示:此时,你应该通过本地测试 SQLLogicTests - #13 to #16。其中 Integration-test-2 requires 需要使用 release 模式运行。

Leaderboard Task (Optional)

对于这个 Project 的 Leaderboard 挑战,我们会提前给你 SQL queries。由你来实现新的 executors 和 optimizer rules,使系统尽可能快地执行这些查询。

Leaderboard 是可选的 (也就是说,你不需要做这个来获得 Project 的满分)。

有可能你在实现 leaderboard test 的优化后,会对现有查询产生不同的优化结果。我们强制要求你在实现新的 optimizer rules 后,必须通过所有测试。同时,我们也会在一些测试案例中强制使用 starter rule。如,为确保你的 index scan executor 正确工作,我们在 sqllogictest 文件中强制使用 starter rule, set force_optimizer_starter_rule=yes

Query 1: Where's the Index?

考虑以下样例数据:

CREATE TABLE t1(x INT, y INT);
CREATE TABLE t2(x INT, y INT);
CREATE TABLE t3(x INT, y INT);
CREATE INDEX t1x ON t1(x);

现在一个用户来了,执行了以下查询。请注意,这个查询与 leaderboard 上的查询不一样,请参考本地测试文件。

SELECT * FROM (t1 INNER JOIN t2 ON t1.x = t2.x) INNER JOIN t3 ON t2.y = t3.y;

即使在 t1.x 上有一个索引,BusTub也没有选择它进行连接!此外,还有两个嵌套的循环连接,这是非常低效的!!

Recommended Optimizations: 使用 hash join 来处理等于条件;join 重排序来挑选 t1 的索引;根据 cardinality (使用EstimatedCardinality函数),先 join t2 和 t3。请注意,在这个 Project 中,hash join 并不要求满分。我们也有一个将 NLJ 转换为 HashJoin 的现有规则,你需要手动启用它。更多信息请参见 optimizer_custom_rules.cpp

Query 2: Too Many Joins!

考虑以下样例数据:

CREATE TABLE t4(x int, y int);
CREATE TABLE t5(x int, y int);
CREATE TABLE t6(x int, y int);

用户写的 SQL 很糟糕。他们忘记了如何写带 join 的查询,所以他们把所有的谓语放在 WHERE 子句中。

SELECT * FROM t4, t5, t6
  WHERE (t4.x = t5.x) AND (t5.y = t6.y) AND (t4.y >= 1000000)
    AND (t4.y < 1500000) AND (t6.x >= 100000) AND (t6.x < 150000);

(与实际的 leaderboard 查询不一样,请参考本地测试文件。我们已经在把一个 filter 下推到实际的 leaderboard 查询中)。

Recommended Optimizations: 分解 filter 条件来提取 hash join keys,将 filter 下推到 hash join 下面来减少对表数据的扫描。

Query 3: The Mad Data Scientist

有一个数据科学家把他们所有的钱都投资在 NFTs 上。在意识到他们的可怕错误后,他们疯了,开始写一些奇怪的 SQL 查询。考虑一下下面的例子:

SELECT v, d1, d2 FROM (
  SELECT v,
         MAX(v1) AS d1, MIN(v1), MAX(v2), MIN(v2),
         MAX(v1) + MIN(v1), MAX(v2) + MIN(v2),
         MAX(v1) + MAX(v1) + MAX(v2) AS d2
    FROM t7 LEFT JOIN (SELECT v4 FROM t8 WHERE 1 == 2) ON v < v4
    GROUP BY v
)

(与实际的 leaderboard 查询不一样,请参考本地测试文件。)

Recommended Optimizations: 列修剪 —— 你只需要在左表中对 v, d1, d2 计算 aggregation, 共同表达式消除, 将总是错误的filter 转换为 dummy scan (values plan node 为 0 rows)。

提示:你不需要实现一个完整的规则来优化这些查询。(1) 完整的谓词下推需要你处理所有的 plan nodes —— limit, order by等。但是为了优化 Q2,你只需要在 hash join / nested loop joins 上实现谓词下推。(2) 一个完整的 join reordering 需要你正确处理谓词 (也许还需要吸收中间的 filters 回到 join谓词),而你不需要这么做。只要让你的优化器能处理这些查询就足够了。

ADDITIONAL INFORMATION

本节提供了一些关于 BusTub 中其他系统组件的额外信息,为了完成本 Project 你需要和它们交互。

System Catalog

数据库维护一个内部 catalog,以跟踪关于数据库的 meta-data 信息。在这个 Project 中,你将与 system catalog 交互,以查询有关 tables、indexes 及其 schemas 的信息。

catalog 的实现全部都在 src/include/catalog/catalog.h 中。你应该特别注意成员函数 Catalog::GetTable()Catalog::GetIndex()。你将在 executor 的实现中使用这些函数来查询 catalog 中的表和索引。

Index Updates

对于会更改表的executors (InsertExecutor and DeleteExecutor),你必须修改操作所针对的表的所有索引。你会发现 Catalog::GetTableIndexes() 函数对于查询为某个特定表定义的所有索引很有用。一旦你拥有表的每个索引的 IndexInfo 实例,你就可以在底层索引结构上调用索引修改操作。

在这个 Project 中,我们使用你在 Project 2 中对 B+Tree 索引的实现作为所有索引操作的底层数据结构。因此,本 Project 的成功完成依赖于 B+Tree 索引的工作实现。

INSTRUCTIONS

本地测试

$ cd build
$ make -j$(nproc) sqllogictest
$ ./bin/bustub-sqllogictest ../test/sql/p3.00-primer.slt --verbose

Task1 要通过本地测试 SQLLogicTests #1 to #5

Task2 要通过本地测试 SQLLogicTests - #6 to #12

Task3 要通过本地测试 SQLLogicTests - #13 to #16。其中 Integration-test-2 requires 需要使用 release 模式运行。

SUBMISSION

$ make format
$ make check-lint
$ make check-clang-tidy-p3
$ make submit-p3

本文作者:Joey-Wang

本文链接:https://www.cnblogs.com/joey-wang/p/17301553.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Joey-Wang  阅读(597)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开