Lab3 - QUERY EXECUTION
實驗三是新增對在資料庫系統中執行查詢的支援。您將實現負責獲取查詢計劃節點並執行它們的executor。您將建立執行下列操作的executor
- Access Methods: Sequential Scans, Index Scans (with your B+Tree from Project #2)
- Modifications: Inserts, Updates, Deletes
- Miscellaneous: Nested Loop Joins, Index Nested Loop Joins, Aggregation, Limit/Offset
擴充閱讀following a select statement through the PostgreSQL internals
Task1 - 熱身任務
資料庫維護一個內部目錄來跟蹤關於資料庫的後設資料。例如,目錄用於回答存在哪些表以及表的位置。更多細節請參見Lecture #04 - Database Storage (Part II).
你需要修改src/include/catalog/catalog.h
允許DBMS新增新表到資料庫中,並使用名稱或內部物件識別符號(table_oid_t
)檢索它們。你將實現以下方法
CreateTable(Transaction *txn, const std::string &table_name, const Schema &schema)
GetTable(const std::string &table_name)
GetTable(table_oid_t table_oid)
您還需要支援向目錄新增索引(基於專案#2 Project #2))。與table_oid_t
類似,索引oid t是索引的唯一物件識別符號。您將實現以下方法
CreateIndex(txn, index_name, table_name, schema, key_schema key_attrs, keysize)
GetIndex(const std::string &index_name, const std::string &table_name)
GetIndex(index_oid_t index_oid)
,GetTableIndexes(const std::string &table_name)
第一個任務官網給的評價是簡單。
1.1 新增新表
其實就圍繞這table的建構函式和原始碼給的資料結構設計就好了
這裡要維護好兩個hash表
- table_name --- table_id
- Table_id --- unique_ptr
TableMetadata *CreateTable(Transaction *txn, const std::string &table_name, const Schema &schema) {
BUSTUB_ASSERT(names_.count(table_name) == 0, "Table names should be unique!");
auto table_id = next_table_oid_++;
TableHeap *_table = new TableHeap(bpm_, lock_manager_,
log_manager_, txn);
names_[table_name] = table_id;
auto new_table = new TableMetadata(schema, table_name, static_cast<std::unique_ptr<TableHeap>>(_table),table_id);
tables_[table_id] = static_cast<std::unique_ptr<TableMetadata>>(new_table);
return new_table;
}
剩下的兩個get函式就是簡單的hash表操作。由於Andy教授不提倡share程式碼。這裡就簡單附上一個實現,剩下的另一個基本沒差
TableMetadata *GetTable(const std::string &table_name) {
if (names_.count(table_name) == 0) {
throw std::out_of_range(table_name);
}
table_oid_t id = names_[table_name];
if (tables_[id] != 0) {
return tables_[id].get();
}
return 0;
}
1.2 新增索引
這裡要注意一點我們新增的索引。是基於我們lab2實現過的b+樹索引。
-
這裡注意我們的
index_Info
函式需要一個Index
的unique_ptr
這裡我們需要new一個BPlusTreeIndex
傳入。 -
我們要對當前表中所有tuple加上index
-
這裡也需要維護好兩個hash表
template <class KeyType, class ValueType, class KeyComparator>
IndexInfo *CreateIndex(Transaction *txn, const std::string &index_name, const std::string &table_name,
const Schema &schema, const Schema &key_schema, const std::vector<uint32_t> &key_attrs,
size_t keysize) {
auto index_id = next_index_oid_++;
auto index_me = new IndexMetadata(index_name, table_name, &schema, key_attrs);
std::unique_ptr<Index> BPlusTree_Index(new BPlusTreeIndex<KeyType, ValueType, KeyComparator>(index_me, bpm_));
IndexInfo *new_index =
new IndexInfo(key_schema, index_name, std::move(BPlusTree_Index), index_id, table_name, keysize);
indexes_[index_id] = static_cast<std::unique_ptr<IndexInfo>>(new_index);
index_names_[table_name].insert(std::unordered_map<std::string, index_oid_t>::value_type (index_name, index_id));
auto table = GetTable(table_name)->table_.get();
// add index for every tuple
for (auto it = table->Begin(txn); it != table->End(); ++it) {
new_index->index_->InsertEntry(it->KeyFromTuple(schema, key_schema, key_attrs), it->GetRid(), txn);
}
return new_index;
}
最後的get操作還是一樣的。附上一個實現
IndexInfo *GetIndex(const std::string &index_name, const std::string &table_name) {
if (index_names_.count(table_name) == 0) {
throw std::out_of_range(table_name);
}
index_oid_t index_id = index_names_[table_name][index_name];
return indexes_[index_id].get();
}
可以通過cmu給出的對於catalog的test。這個測試檔案就是線上評測用的測試檔案。
Task2 - EXECUTORS
在第二個任務中,您將實現執行程式以進行順序掃描,插入,雜湊聯接和聚合。 對於每種查詢計劃運算子型別,都有一個相應的executor物件,該物件實現Init和Next方法。 Init方法用於設定有關操作呼叫的內部狀態(例如,檢索要掃描的對應表)。 Next方法提供了迭代器介面,該介面在每次呼叫時返回一個元組(如果沒有更多的元組,則返回null)。
你需要修改下面的檔案來完成這一任務。
src/include/execution/executors/seq_scan_executor.h
src/include/execution/executors/index_scan_executor.h
src/include/execution/executors/insert_executor.h
src/include/execution/executors/update_executor.h
src/include/execution/executors/delete_executor.h
src/include/execution/executors/nested_loop_join_executor.h
src/include/execution/executors/nested_index_join_executor.h
src/include/execution/executors/aggregation_executor.h
src/include/execution/executors/limit_executor.h
我們假設執行器在整個專案中都是單執行緒的。您還可以根據需要隨意新增私有函式和類成員。
我們提供了ExecutionEngine(src / include / execution / execution_engine.h)
作為幫助類。 它將輸入的查詢計劃轉換為查詢執行程式,並執行直到收集所有結果。 您將需要修改ExecutionEngine
以捕獲執行程式引發的任何異常。
若要了解如何在查詢執行期間建立excutors
,請參閱ExecutorFactory(src / include / execution / executor_factory.h)
幫助程式類。 此外,每個執行程式都有一個ExecutorContext(src / include / execution / executor_context.h)
作為執行上下文環境。
SEQUENTIAL SCANS
順序掃描遍歷一個表並每次返回一個元組。順序掃描由SeqScanPlanNode指定。plan節點指定要迭代的表。節點還可以包含一個predicate
;如果一個元組不滿足predicate
,則跳過它。
提示:您需要使用順序掃描中的predicate
來判斷。特別需要注意的是AbstractExpression::Evaluate
。會返回Value。需要使用GetAs<bool>
變成bool型別。
實現
這個實驗其實並不難。重點就是實現之前要先梳理一下整個系統的一些設計模式。
以這一行為例利用了工廠設計模式
auto executor = ExecutorFactory::CreateExecutor(exec_ctx, plan);
整個順序掃描的思路非常簡單。就是從頭開始遍歷整個表的所有tuple。找到滿足要求的tuple。並將tuple中的滿足要求的value和outSchema
組合成新的tuple
存入result就ok。
這裡注意由於要進行遍歷。所以要引入TableIterator
。
這裡附上最重要的Init
實現。由於要遍歷表的所有tuple。所以理應有一個table_info
。同時要遍歷整個表所以我們需要一個迭代器
void SeqScanExecutor::Init() {
table_heap_ = exec_ctx_->GetCatalog()->GetTable(plan_->GetTableOid())->table_.get();
iter = table_heap_->Begin(exec_ctx_->GetTransaction());
}
另一個實現就是一個tuple(其實就是一行)中的元素並不一定都是我們想要的。所以要有一個從tuple
中獲取和ouSchema
中對應列的元素的函式這裡就叫它getValuesFromTuple
std::vector<Value> SeqScanExecutor::getValuesFromTuple(const Tuple *tuple, const Schema *schema) {
std::vector<Value> res;
for (const Column &col : schema->GetColumns()) {
Value val = tuple.GetValue(schema, schema->GetColIdx(col.GetName()));
res.push_back(val);
}
return res;
}
INDEX SCANS
就是利用加了索引的順序掃描。
實現
這個基本和上面的類似。只不過利用了索引
index_key = tuple_.KeyFromTuple(table_info->schema_, index_info->key_schema_, index_info->index_->GetKeyAttrs());
index_Expression->Evaluate(&index_key, &table_info->schema_).GetAs<bool>()
INSERT
插入將元組新增到表中。插入由InsertPlanNode指定。有兩種型別的插入:1. 直接插入(只有插入語句),2. 非直接插入(從子語句中獲取要插入的值)。例如INSERT INTO empty_table2 SELECT colA, colB FROM test_1 WHERE colA > 500
需要先執行後面的select語句在進行insert。
實現
- 先判斷一下是否只有一個plan。
- 如果是的話則非常簡單的插入就好了
if (is_rawInsert && num_inserted < num_values) {
bool inserted = table_info->table_->InsertTuple(Tuple(raw_vals[num_inserted], &table_info->schema_), rid, exec_ctx_->GetTransaction());
BUSTUB_ASSERT(inserted, "Sequential insertion cannot fail");
for (auto index : table_Indexs) {
index->index_->InsertEntry(Tuple(raw_vals[num_inserted], &table_info->schema_), *rid, exec_ctx_->GetTransaction());
}
num_inserted++;
return true;
}
- 否則的話則需要先執行後面的子
plan
注意對於insert只能有一個子plan。這樣就無需關注子plan的順序問題
先執行子查詢語句
execution_engine_ = std::make_unique<ExecutionEngine>(GetExecutorContext()->GetBufferPoolManager(), GetExecutorContext()->GetTransactionManager(),
GetExecutorContext()->GetCatalog());
auto pNode = plan_->GetChildPlan();
execution_engine_->Execute(pNode, &result_set,GetExecutorContext()->GetTransaction(),GetExecutorContext());
然後在進行插入
這裡注意插入的時候我們value是直接從子計劃獲得的result_set
中獲取
bool inserted = table_info->table_->InsertTuple(*result_set.begin(), rid, exec_ctx_->GetTransaction());
BUSTUB_ASSERT(inserted, "Sequential insertion cannot fail");
for (auto index : table_Indexs) {
index->index_->InsertEntry(*result_set.begin(), *rid, exec_ctx_->GetTransaction());
}
result_set.erase(result_set.begin());
return true;
UPDATE
Update修改指定表中的現有元組並更新其索引。UpdatePlanNode可以利用SeqScanPlanNode或IndexScanPlanNode來提供進行更新的目標元組如
- 先向
empty_table2
表中插入資料 - 然後修改
colA
的值
INSERT INTO empty_table2 SELECT colA, colA FROM test_1 WHERE colA < 50
UPDATE empty_table2 SET colA = colA+10 WHERE colA < 50
實現
這裡也是附上比較重要的next函式實現
bool UpdateExecutor::Next([[maybe_unused]] Tuple *tuple, RID *rid) {
if (child_executor_->Next(tuple, rid)) {
*tuple = GenerateUpdatedTuple(*tuple);
return table_info_->table_.get()->UpdateTuple(*tuple, *rid, txn);
}
return false;
}
Delete
delete的邏輯都是非常簡單的。就是找到滿足要求的tuple,然後將其刪除。這裡還要注意出了刪除tuple還要刪除對應的index
實現
std::vector<RID> rids;
index->index_->ScanKey(key, &rids, txn);
table_info_->table_->MarkDelete(rids[0], txn);
index->index_->DeleteEntry(key, key.GetRid(), txn);
JOIN
join操作用於將兩個子執行語句的結果組合在一起。在這個任務中你需要實現兩種形式的join操作一種是基本的join另外一種是基於索引的join操作
實現
1. 基本join操作
這個實現非常簡單,就是二重迴圈。對於外查詢中的每一個元素在內表中進行遍歷看一下是否有相等的。
SELECT test_1.colA, test_1.colB, test_2.col1, test_2.col3 FROM test_1 JOIN test_2 ON test_1.colA = test_2.col1 AND test_1.colA < 50
比如對於上面這個語句。我們要先把後面的select
語句執行完。然後就是一個二重迴圈
const Schema *left_schema = left_executor_->GetOutputSchema();
const Schema *right_schema = right_executor_->GetOutputSchema();
for (const auto &p1 : left_tuples) {
for (const auto &p2 : right_tuples) {
Tuple left_tuple = p1;
Tuple right_tuple = p2;
if (plan_->Predicate()->EvaluateJoin(&left_tuple, left_schema, &right_tuple, right_schema).GetAs<bool>()) {
std::vector<Value> left_values = getValuesFromTuple(&left_tuple, left_schema);
std::vector<Value> right_values = getValuesFromTuple(&right_tuple, right_schema);
left_values.insert(left_values.end(), right_values.begin(), right_values.end());
res.emplace_back(left_values, plan_->OutputSchema());
}
}
}
2. 利用index的join操作
主要有兩個注意點。獲得key_tuple
和在index中利用key_tuple找到rid
Tuple key_tuple =
outer_tuple.KeyFromTuple(*outer_table_schema, *index_key_schema, inner_index_->index_->GetKeyAttrs());
inner_index_->index_->ScanKey(key_tuple, &rids, exec_ctx_->GetTransaction());
別的基本邏輯和上面的類似了不能貼太多程式碼啊。
AGGREGATION
聚合用於將來自單個子執行器的多個元組結果組合為單個元組。在這個專案中,我們要求您實現COUNT、SUM、MIN和MAX。
我們為您提供了一個SimpleAggregationHashTable。我們強烈建議您使用這個雜湊表。
實現
SimpleAggregationHashTable
這個hash表提供了一系列操作。下面我們依次來看一下。
1.為聚合操作提供不同的初始值
可以發現這個函式為count和sum操作提供的初始值為0。而為求最小值操作提供的初始值是32位的最大值。max函式則提供了32位的最小值。這都是符合我們思維的
2. 這個函式主要是為了構建一個迭代器方便後面的having group by操作
3. 幫你實現聚合操作的函式
注意這裡的hash表。就是groupby操作用的
std::unordered_map<AggregateKey, AggregateValue> ht{};
它的key就是group by操作的列。這樣就可以實現去重操作。嗯設計的真不錯
因此整個程式碼的核心就在Init函式對於這個hash表和迭代器的構建
void AggregationExecutor::Init() {
auto childE = child_.get();
{
childE->Init();
try {
Tuple tuple;
RID rid;
while (childE->Next(&tuple, &rid)) {
aht_.InsertCombine(MakeKey(&tuple), MakeVal(&tuple));
}
} catch (Exception &e) {
throw "you met error";
}
aht_iterator_ = aht_.Begin();
}
}
無內鬼通過啦。
後面就是把之前的筆記補一下。然後開始看最後一個project的課。然後最後一個實驗了加油。所以的都搞完之後準備整理一個對這個專案的整理(後面面試用)