[已滿分]CMU數據庫(15-445)實驗2-b+樹索引實現(上)


Lab2

在做實驗2之前請確保實驗1結果的正確性。不然你的實驗2將無法正常進行

環境搭建地址如下 https://www.cnblogs.com/JayL-zxl/p/14307260.html

實驗一的地址如下 https://www.cnblogs.com/JayL-zxl/p/14311883.html

實驗的地址如下 https://15445.courses.cs.cmu.edu/fall2020/project2/

0. 寫在前面

嘿嘿周小倫重新把lab梳理一遍。同時修改一下之前出現的錯誤。和博客中不完善的地方。

1. 實驗介紹

第一個打分點---實現b+樹的基本結構、插入、搜索操作

注意這里沒有考慮打分點2的並發問題,所以對於加鎖、解鎖和事物都沒有考慮。

第二個打分點--實現b+樹的刪除操作、索引迭代器和對並發訪問的支持

Task 1 B+TREE PAGES

您需要實現三個頁面類來存儲B+樹的數據。

  • B+ Tree Parent Page
  • B+ Tree Internal Page
  • B+ Tree Leaf Page

1. B+ Tree Parent Page

這是內部頁和葉頁都繼承的父類,它只包含兩個子類共享的信息。父頁面被划分為如下表所示的幾個字段。
*B+Tree Parent Page Content

Variable Name Size Description
page_type_ 4 Page Type (internal or leaf)
lsn_ 4 Log sequence number (Used in Project 4)
size_ 4 Number of Key & Value pairs in page
max_size_ 4 Max number of Key & Value pairs in page
parent_page_id_ 4 Parent Page Id
page_id_ 4 Self Page Id

您必須在指定的文件中實現您的父頁。您只能修改頭文件(src/include/storage/page/b_plus_tree_page.h) 和其對應的源文件 (src/storage/page/b_plus_tree_page.cpp).

這里都是一些簡單的set、get就不寫出來了

2. B+TREE INTERNAL PAGE

內部頁不存儲任何實際數據,而是存儲有序的m個鍵條目和m + 1個指針(也稱為page_id)。 由於指針的數量不等於鍵的數量,因此將第一個鍵設置為無效,並且查找方法應始終從第二個鍵開始。 任何時候,每個內部頁面至少有一半已滿。 在刪除期間,可以將兩個半滿頁面合並為合法頁面,或者可以將其重新分配以避免合並,而在插入期間,可以將一個完整頁面分為兩部分。

你只能修改頭文件(src/include/storage/page/b_plus_tree_internal_page.h) 和對應的源文件(src/page/b_plus_tree_internal_page.cpp).

* Internal page format (keys are stored in increasing order):
*  --------------------------------------------------------------------------
* | HEADER | KEY(1)+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) |
*  --------------------------------------------------------------------------
#define INDEX_TEMPLATE_ARGUMENTS template <typename KeyType, typename ValueType, typename KeyComparat>

3. B+TREE LEAF PAGE

葉子頁存儲有序的m個鍵條目(key)和m個值條目(value)。 在您的實現中,值只能是用於定位實際元組存儲位置的64位record_id,請參閱src / include / common / rid.h中定義的RID類。 葉子頁與內部頁在鍵/值對的數量上具有相同的限制,並且應該遵循相同的合並,重新分配和拆分操作。您必須在指定的文件中實現內部頁。 僅允許您修改頭文件(src / include / storage / page / b_plus_tree_leaf_page.h)及其相應的源文件(src / storage / page / b_plus_tree_leaf_page.cpp)。

‼️重要的KeyIndex函數

這個函數可以返回第一個>=當前key值的編號。這個在插入的時候經常會用到,這樣就可以讓代碼重復利用,在LevelDB中也有類似的操作。

INDEX_TEMPLATE_ARGUMENTS
int B_PLUS_TREE_LEAF_PAGE_TYPE::KeyIndex(const KeyType &key, const KeyComparator &comparator) const {
  // 二分查找
  // TODO : 換成std::lower_bound()好看一點
  int l = 0;
  int r = GetSize();
  if (l >= r) {
    return GetSize();
  }
  while (l < r) {
    int mid = (l + r) / 2;
    if (comparator(array_[mid].first, key) < 0) {
      l = mid + 1;
    } else {
      r = mid;
    }
  }
  return l;
}

重要提示:盡管葉子頁和內部頁包含相同類型的鍵,但它們可能具有不同類型的值,因此葉子頁和內部頁的最大大小可能不同。每個B + Tree葉子/內部頁面對應從緩沖池獲取的存儲頁面的內容(即data_部分)。 因此,每次嘗試讀取或寫入葉子/內部頁面時,都需要首先使用其唯一的page_id從緩沖池中提取頁面,然后將其重新解釋為葉子或內部頁面,並在寫入或刪除后執行unpin操作。

其實就是實現b_plus_tree.cpp/InsertIntoLeaf函數所涉及到的相關函數。

您的B +樹索引只能支持唯一鍵。 也就是說,當您嘗試將具有重復鍵的鍵值對插入索引時,它應該返回false

對於checkpoint1,僅需要B + Tree索引支持插入(Insert)和點搜索(GetValue)。 您不需要實現刪除操作。 插入后如果當前鍵/值對的數量等於max_size,則應該正確執行分割。 由於任何寫操作都可能導致B + Tree索引中的root_page_id發生更改,因此您有責任更新(src / include / storage / page / header_page.h)中的root_page_id,以確保索引在磁盤上具有持久性 。 在BPlusTree類中,我們已經為您實現了一個名為UpdateRootPageId的函數。 您需要做的就是在B + Tree索引的root_page_id更改時調用此函數。

您的B + Tree實現必須隱藏key/value等的詳細信息,建議使用如下結構:

template <typename KeyType,
          typename ValueType,
          typename KeyComparator>
class BPlusTree{
   // ---
};

這些類別已經為你實現了

  • KeyType: The type of each key in the index. This will only be GenericKey, the actual size of GenericKey is specified and instantiated with a template argument and depends on the data type of indexed attribute.

  • ValueType: The type of each value in the index. This will only be 64-bit RID.

  • KeyComparator: The class used to compare whether two KeyType instances are less/greater-than each other. These will be included in the KeyType implementation files.

  1. 你必須使用傳入的transaction,把已經加鎖的頁面保存起來。
  2. 我們提供了讀寫鎖存器的實現(src / include / common / rwlatch.h)。 並且已經在頁面頭文件下添加了輔助函數來獲取和釋放Latch鎖(src / include / storage / page / page.h)。

首先附上書上的b+樹插入算法

image-20210124184901398

對上面幾種情況的分析

1. 如果當前為空樹則創建一個葉子結點並且也是根節點

  • 這里是leaf結點所以這里需要用到leaf page內的函數

  • 注意這里需要用lab1實現的buffer池管理器來獲得page。 這里記得創建完新的結點之后要unpin

  • 進行插入的時候用二分插入來進行優化

1. 創建新結點

INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::StartNewTree(const KeyType &key, const ValueType &value) {
  Page *new_page = buffer_pool_manager_->NewPage(&root_page_id_);
  if (new_page == nullptr) {
    throw "out of memory";
  }

  LeafPage *leaf_page = reinterpret_cast<LeafPage *>(new_page);
  leaf_page->Init(root_page_id_,INVALID_PAGE_ID,leaf_max_size_);

  //update root page id
  UpdateRootPageId(1);
  // Insert entry directly into leaf page.
  // For a new B+ tree, the root page is the leaf page.
  leaf_page->Insert(key, value, comparator_);
  buffer_pool_manager_->UnpinPage(root_page_id_, true);  // unpin
}

2. insert函數

這里的insert函數可以直接用之前的KeyIndex函數

INDEX_TEMPLATE_ARGUMENTS
int B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, const KeyComparator &comparator) {
  // 1. 邊界判斷
  if (GetSize() == GetMaxSize()) {
    std::cout << "leaf_page Insert: size=" << GetSize() << ", max_size=" << GetMaxSize() << std::endl;
  }
  // 2. find position
  int pos = KeyIndex(key, comparator);
  // 3. insert
  // make room
  for (int i = GetSize() - 1; i >= pos; i--) {
    array_[i + 1] = array_[i];
  }

  array_[pos] = MappingType{key, value};
  // 4. update size
  IncreaseSize(1);
  return GetSize();
}

2. 否則尋找到插入元素應該在的葉子結點,並插入(不分裂)

  1. 首先找到葉子結點
  2. 如果葉子結點內的元素個數小於最大值則直接插入
  3. 否則需要進行分裂。產生兩個新的結點。把元素上提
  4. 如果提到父親結點,父結點仍需要分裂。則遞歸進行分裂否則結束

如果葉子結點內的關鍵字小於m-1,則直接插入到葉子結點

1. LookUp函數實現

Lookup函數用來尋找包含輸入"key"的children pointer(其實就是page_id)

INDEX_TEMPLATE_ARGUMENTS
ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const {
  // 從第二個節點開始
  for (int i = 1; i < GetSize(); i++) {
    KeyType cur_key = array_[i].first;
    if (comparator(key, cur_key) < 0) {
      return array_[i - 1].second;
    }
  }
  return array_[GetSize() - 1].second;
}

2. findLeafPage

由於要找到應該插入的LeafPage所以這個函數狠狠重要。但是這里是非並發下插入,在這里用findLeafPage進行對插入算法的測試。后面對於並發情況會有所修改。

  1. 從整個b+樹的根節點開始。一直向下找到葉子結點
  2. 因為b+樹是多路搜索樹,所以整個向下搜索就是通過key值進行比較
  3. 其中內部結點向下搜索的過程利用了上面提到的lookup函數
// only need for inserting test
INDEX_TEMPLATE_ARGUMENTS
Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost) {
  throw Exception(ExceptionType::NOT_IMPLEMENTED, "Implement this for test");
  if (root_page_id_ == INVALID_PAGE_ID) {
    throw std::runtime_error("Unexpected. root_page_id is INVALID_PAGE_ID");
  }
  Page *page = buffer_pool_manager_->FetchPage(root_page_id_);  // now root page is pin
  BPlusTreePage *node = reinterpret_cast<BPlusTreePage *>(page);
  while (!node->IsLeafPage()) {
    InternalPage *internal_node = reinterpret_cast<InternalPage *>(node);
    page_id_t next_page_id = leftMost ? internal_node->ValueAt(0) : internal_node->Lookup(key, comparator_);
    Page *next_page = buffer_pool_manager_->FetchPage(next_page_id);  // next_level_page pinned
    BPlusTreePage *next_node = reinterpret_cast<BPlusTreePage *>(next_page);
    buffer_pool_manager_->UnpinPage(node->GetPageId(), false);  // curr_node unpinned
    page = next_page;
    node = next_node;
  }
  return page;
}

3. 無分裂直接插入

INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
  // 不考慮鎖的實現
  {
    if (IsEmpty()) {
      StartNewTree(key, value);
      return true;
    }
    //[Attention] 這里獲取到page是pined
    Page *right_leaf = FindLeafPage(key, false);
    LeafPage *leaf_page = reinterpret_cast<LeafPage *>(right_leaf);

    // 1. if insert key is exist
    if (leaf_page->Lookup(key, nullptr, comparator_)) {
      buffer_pool_manager_->UnpinPage(leaf_page->GetPageId(), false); // unpined page
      return false;
    }
    // 2. insert entry
    leaf_page->Insert(key, value, comparator_);
    // 下面分析需要分裂的情況

3. 分裂的情況

InsertLeaf主函數接上文。

// 2.1 need to split
    // split need to do two things
    // 1. create new page copy [mid, r] to new page
    // 2. if necessary 遞歸處理
    if (leaf_page->GetSize() == maxSize(leaf_page) + 1) {
      LeafPage *new_leaf = Split(leaf_page);
      InsertIntoParent(leaf_page, new_leaf->KeyAt(0), new_leaf, transaction);
    }
    // 2.2 no need to split
    // must unpin right leaf page
    buffer_pool_manager_->UnpinPage(right_leaf->GetPageId(), true);
    return true;

1. 調用split函數對葉子結點進行分割

  1. split的時候會產生一個含有m-m/2個關鍵字的新結點。注意把兩個葉子結點連接起來。
  2. 這里注意split函數要區分葉子結點和內部結點。因為葉子結點需要更新雙向鏈表
INDEX_TEMPLATE_ARGUMENTS
template <typename N>
N *BPLUSTREE_TYPE::Split(N *node) {
  // 1, ask a new page
  page_id_t new_page_id;
  Page *new_page = buffer_pool_manager_->NewPage(&new_page_id);  // pinned
  if (new_page == nullptr) {
    throw std::string("out of memory");
  }
  N *new_node;
  if (node->IsLeafPage()) {
    LeafPage *leaf_node = reinterpret_cast<LeafPage *>(node);
    LeafPage *new_leaf_node = reinterpret_cast<LeafPage *>(new_page);
    new_leaf_node->Init(new_page_id, leaf_node->GetParentPageId(),leaf_max_size_);
    leaf_node->MoveHalfTo(new_leaf_node);
    // 葉子節點更新雙向鏈表
    new_leaf_node->SetNextPageId(leaf_node->GetNextPageId());
    leaf_node->SetNextPageId(new_leaf_node->GetPageId());
    new_node = reinterpret_cast<N *>(new_leaf_node);
  } else {
    // 內部節點不需要設置雙向鏈表
    InternalPage *internal_node = reinterpret_cast<InternalPage *>(node);
    InternalPage *new_internal_node = reinterpret_cast<InternalPage *>(new_page);
    new_internal_node->Init(new_page_id, internal_node->GetParentPageId(),internal_max_size_);
    internal_node->MoveHalfTo(new_internal_node,buffer_pool_manager_);
    new_node = reinterpret_cast<N *>(new_internal_node);
  }
  return new_node;
}

這里涉及到了MoveHalfTo函數簡單的附上一下,這個非常簡單

INDEX_TEMPLATE_ARGUMENTS
void B_PLUS_TREE_LEAF_PAGE_TYPE::MoveHalfTo(BPlusTreeLeafPage *recipient) {
  // 這里好像是說 你左邊多還是右邊多都行,書上是左邊多,我個人習慣右邊多
  int moved_num = GetSize() - GetSize() / 2;
  int start = GetSize() - moved_num;
  CopyNFrom(array_ + start, moved_num);
  IncreaseSize(-1 * moved_num);
  recipient->IncreaseSize(moved_num);
}

2. InsertIntoParent函數實現

這個函數的實現先看一下書上給出的算法

  1. 如果old_node就是根節點,那么就要創建一個新的節點R當作根節點。然后取key的值當作根節點的值。修改old_nodenew_node的父指針。以及根節點的孩子指針
 // 1. old_node is root
  if (old_node->IsRootPage()) {
    page_id_t new_root_pid;
    Page *page = buffer_pool_manager_->NewPage(&new_root_pid);
    InternalPage *new_root_page = reinterpret_cast<InternalPage *>(page->GetData());

    // new root page init
    new_root_page->Init(new_root_pid, INVALID_PAGE_ID, internal_max_size_);
    UpdateRootPageId(0);  // update not insert
    root_page_id_ = new_root_pid;
    // set parent page id
    new_root_page->PopulateNewRoot(old_node->GetPageId(), key, new_node->GetPageId());
    old_node->SetParentPageId(new_root_pid);
    new_node->SetParentPageId(new_root_pid);
    buffer_pool_manager_->UnpinPage(new_root_pid, true);
  }
  1. 找到分裂的葉子結點的父親節點隨后進行判斷

a. 如果可以直接插入則直接插入

b. 否則需要對父結點在進行分裂,即遞歸調用。

else {
    page_id_t parent_pid = old_node->GetParentPageId();
    Page *parent_page = buffer_pool_manager_->FetchPage(parent_pid);  // parent_page pined
    InternalPage *parent_node = reinterpret_cast<InternalPage *>(parent_page->GetData());

    parent_node->InsertNodeAfter(old_node->GetPageId(), key, new_node->GetPageId());

    if (parent_node->GetSize() == maxSize(parent_node) + 1) {
      // need to split
      InternalPage *new_parent_node = Split(parent_node);  // new_parent_node pined
      InsertIntoParent(parent_node, new_parent_node->KeyAt(0), new_parent_node);

      buffer_pool_manager_->UnpinPage(new_parent_node->GetPageId(), true);  // unpin new_parent_node
    }
    buffer_pool_manager_->UnpinPage(parent_node->GetPageId(), true);  // unpin parent_node
  }

好了第一部分的測試就通過了

image-20210124230210652

附上一個pass的截圖完成第一部分✅
如果我們插入1、2、3、4、5那么我們用程序得到的結果如下

可以發現是完全正確的 🌟
附上通過的cmu網站滿分測試

3. ⚠️一些細節

1. 關於內部結點和葉子結點的區別

1.1 大小不一樣

內部結點的最大結點個數是比葉子結點多一

例如m = 3, 那么內部結點的個數就可以是3。而葉子結點則最多是2,但是內部結點的array[0]實際上就是個存地址的。它的key在我們的Draw結果圖中都不顯示。

1.2 在Split的時候有區別

  • 在葉子結點split的時候需要進行雙向鏈表的維護
  • 而在內部結點則不需要
  • 共有操作都是獲得一個新頁--> 類型轉換 ---> MoveHalfTo

2. upin的pin的注意事項

  • 當你利用FetchPage拿到一個page的時候他就是pined
  • 當你使用完之后記得要unpin這很重要

3. debug的一些小技巧

  • 利用可視化網站和代碼中給的b_plus_print_test這個測試,把輸入圖打印成xxx.dot然后復制里面的內容在http://dreampuf.github.io/GraphvizOnline/顯示進行對比。
  • 對於Mac系統利用Clion可以直接對測試文件debug。還是非常爽的。其中lldb的利用非常重要。

4. maxSize的含義

這里要注意在進行B+樹初始化時候給的

internal_max_size可以認為指的是指針數。也就是說假設我們有m = 3的b+樹

inter_max_size = 3 是可以有三個key。但是 leaf_max_size = 2 就只能包含一個key。這個在測試用例被卡才發現的。

所以maxSize()函數可以這樣實現

INDEX_TEMPLATE_ARGUMENTS
template <typename N>
int BPLUSTREE_TYPE::maxSize(N *node) {
  return node->IsLeafPage() ? leaf_max_size_ - 1 : internal_max_size_;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM