Lab 5 B+ Tree Index
本次lab就是要实现B+树索引,在lab 1中实现的HeapFile实际上实现的是顺序索引,但真正数据库使用的一般都是B+树索引,或者像leveldb使用跳表作为索引。如果需要补充B树或者B+树的知识可以看下面两个链接,B树,B+树。
Exercise 1 实现BTreeFile.findLeafPage()
-
首先我们需要明确的是findLeafPage函数做的是什么,该函数提供的功能是,返回所需节点的所在页面,也就是找到该节点所在的叶子节点的页面。同时在函数内部实现递归,不断的向下查找到叶子节点页面。根据lab 5的实验文档,我们可以将查找节点的页面分为三种情况:
- 当传进来的页面就是叶子节点,那么就表示找到了所需节点的页面,直接调用getPage函数从bufferpool中获取该页面。
- 当寻找的节点为null时,则递归的寻找最左侧的叶子节点所对应的页面。
- 当传进来的页面是非叶子节点的页面且寻找的节点不为null,判断所需寻找的节点与当前非叶子节点内的key进行判断,如果key大于等于寻找的节点,则返回当前key的左子树。
-
代码实现如下:
-
private BTreeLeafPage findLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreePageId pid, Permissions perm, Field f) throws DbException, TransactionAbortedException { // some code goes here if(pid.pgcateg() == BTreePageId.LEAF){ return (BTreeLeafPage) getPage(tid,dirtypages,pid,perm); } else if (pid.pgcateg() == BTreePageId.INTERNAL){ BTreeInternalPage page = (BTreeInternalPage) getPage(tid,dirtypages,pid,perm); Iterator<BTreeEntry> bTreeEntryIterator = page.iterator(); if(bTreeEntryIterator == null || !bTreeEntryIterator.hasNext()) throw new DbException("entry不存在"); if(f == null) return findLeafPage(tid, dirtypages, bTreeEntryIterator.next().getLeftChild(), perm, f); BTreeEntry entry = null; while (bTreeEntryIterator.hasNext()){ entry = bTreeEntryIterator.next(); if(entry.getKey().compare(Op.GREATER_THAN_OR_EQ,f)) return findLeafPage(tid, dirtypages, entry.getLeftChild(), perm, f); } return findLeafPage(tid, dirtypages, entry.getRightChild(), perm, f); } return null; }
Exercise 2 插入实现
-
虽然本次exercise是实现插入的算法,但是都是实现的插入后的分裂算法,根据exercise 1的寻找叶子节点,我们能很轻松的找到我们需要插入到哪个页面,但是当插入的个数大于m个时,该entry需要进行分裂。本exercise就是根据叶子entry 和 非叶子entry进行分别分裂函数的实现。
-
首先是关于叶子节点的分裂,分裂都是将部分节点移动到新的右叶子页面中。所以函数实现步骤如下:
- 创建新的叶子页面,将当前传入的需要分裂的叶子的反向迭代器取出。
- 将迭代器中的一半tuple插入,新的右叶子页面中。
- 之后判断需要分裂的叶子页面是否还有右邻居,如果有将右邻居的左指针(这里其实不一定是指针,可能是一个标志位),指向新的新的右叶子页面。
- 然后将新的右叶子页面的左节点指向分裂页面,将右节点指向旧的右页面。
- 最后将新的叶子节点的页面的第一个值作为key更新到父节点上。
-
代码实现如下:
-
public BTreeLeafPage splitLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreeLeafPage page, Field field) throws DbException, IOException, TransactionAbortedException { // some code goes here // // Split the leaf page by adding a new page on the right of the existing // page and moving half of the tuples to the new page. Copy the middle key up // into the parent page, and recursively split the parent as needed to accommodate // the new entry. getParentWithEmtpySlots() will be useful here. Don't forget to update // the sibling pointers of all the affected leaf pages. Return the page into which a // tuple with the given key field should be inserted. BTreeLeafPage rightPage = (BTreeLeafPage) getEmptyPage(tid, dirtypages, BTreePageId.LEAF); Iterator<Tuple> tuples = page.reverseIterator(); int tupleNum = page.getNumTuples(); // 把后一半的tuple移动到rightPage上 for(int i=0; i < tupleNum / 2; ++i) { Tuple tuple = tuples.next(); page.deleteTuple(tuple); rightPage.insertTuple(tuple); } // 如果分裂的page有右邻居 if(page.getRightSiblingId() != null) { BTreePageId oldRightId = page.getRightSiblingId(); BTreeLeafPage oldRightPage = (BTreeLeafPage) getPage(tid, dirtypages, oldRightId, Permissions.READ_WRITE); oldRightPage.setLeftSiblingId(rightPage.getId()); } rightPage.setLeftSiblingId(page.getId()); rightPage.setRightSiblingId(page.getRightSiblingId()); page.setRightSiblingId(rightPage.getId()); Field key = rightPage.iterator().next().getField(keyField); BTreeEntry entry = new BTreeEntry(key, page.getId(), rightPage.getId()); BTreeInternalPage parentPage = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), key); parentPage.insertEntry(entry); updateParentPointers(tid, dirtypages, parentPage); return (field.compare(Op.GREATER_THAN_OR_EQ, key)? rightPage : page); }
-
其次就是关于非叶子节点的分裂,分裂还是将部分节点移动到新的右叶子页面。所以函数实现步骤如下:
- 创建新的非叶子节点,将当前传入的需要分裂的叶子的反向迭代器取出。
- 将迭代器的一半tuple插入,新的右叶子页面中。
- 然后将迭代器n/2的tuple取出,然后在分裂页面中将其删除,同时将其更新到父节点中,即创建新的Entry,插入到上层的BTreeInternalPage中。
- 在dirtypages中更新页面,并返回field所在的页面。
-
public BTreeInternalPage splitInternalPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreeInternalPage page, Field field) throws DbException, IOException, TransactionAbortedException { // some code goes here // // Split the internal page by adding a new page on the right of the existing // page and moving half of the entries to the new page. Push the middle key up // into the parent page, and recursively split the parent as needed to accommodate // the new entry. getParentWithEmtpySlots() will be useful here. Don't forget to update // the parent pointers of all the children moving to the new page. updateParentPointers() // will be useful here. Return the page into which an entry with the given key field // should be inserted. BTreeInternalPage rightPage = (BTreeInternalPage) getEmptyPage(tid, dirtypages, BTreePageId.INTERNAL); Iterator<BTreeEntry> BtreeEntries = page.reverseIterator(); if(BtreeEntries == null || !BtreeEntries.hasNext()) throw new DbException("Internal Page has no entry!"); int numEntry = page.getNumEntries(); for(int i=0; i<numEntry/2; ++i) { BTreeEntry entry = BtreeEntries.next(); page.deleteKeyAndRightChild(entry); rightPage.insertEntry(entry); } BTreeEntry e = BtreeEntries.next(); Field key = e.getKey(); page.deleteKeyAndRightChild(e); //push the key up to the parent page BTreeEntry newEntry = new BTreeEntry(key, page.getId(), rightPage.getId()); BTreeInternalPage parentPage = getParentWithEmptySlots(tid, dirtypages, page.getParentId(), key); parentPage.insertEntry(newEntry); updateParentPointers(tid, dirtypages, parentPage); updateParentPointers(tid, dirtypages, rightPage); return field.compare(Op.GREATER_THAN_OR_EQ, key)? rightPage:page; }
Exercise 3 重新分发页面的实现
-
在删除元素中会遇到将B+树结构操作的不平衡,这时就涉及到了,将页面进行重新分配和合并页面的操作。本次exercise就是关于重新分发页面的函数实现。
-
在叶子节点重新分配,需要考虑多tuple的那个页面,是在被插入的页面的左边还是右边,这是为什么呢,因为在B+树底层插入的时候需要保持数据是有序的,所以会产生一下两种情况:
- 当被插入页面在左边的时候,那多出来的tuple的页面就在右边,那么我们就希望获取正向迭代器。这样的话,就在insertTuple函数中可以直接逐个插入在最后面,就不需要很多判断减少判断的耗时。
- 当被插入页面在右边的时候,那多出来的tuple的页面就在左边,那么我们就希望获取反向迭代器。这样的话,在insertTuple函数中可以直接插在最前面且一直是插在最前面,这样判断就降到了常数级。
最后将多出来的那截tuple插入待插入的页面,最后将最后插入那个key提到parent更新。
-
代码实现如下:
-
public void stealFromLeafPage(BTreeLeafPage page, BTreeLeafPage sibling, BTreeInternalPage parent, BTreeEntry entry, boolean isRightSibling) throws DbException { // some code goes here // // Move some of the tuples from the sibling to the page so // that the tuples are evenly distributed. Be sure to update // the corresponding parent entry. Iterator<Tuple> moveTuple = isRightSibling? sibling.iterator(): sibling.reverseIterator(); int numSteal = (sibling.getNumTuples() - page.getNumTuples())/2; Tuple t = null; for(int i=0; i<numSteal; ++i) { t = moveTuple.next(); sibling.deleteTuple(t); page.insertTuple(t); } assert t != null; entry.setKey(t.getField(keyField)); parent.updateEntry(entry); }
-
非叶子节点就比较复杂了,这里分为两种情况,当被插入的页面在右边的时候,同样的根据在叶子节点重新分配的逻辑,我们需要反向迭代器,然后计算出需要插入多少entry。
-
这时就涉及到一个问题,**需要将父节点拿下来插入到被插入的页面中,则需要构建一个新的entry,这个entry key是父节点的值,leftChild就是多出来的entry页面中的最后一个entry的右孩子,rightChild就是被插入页面中的第一个entry的左孩子。**这样我们就将父节点构造成了一个可插入的entry,并把它插入被插入页面。
-
接着就循环插入多出来的entry,这些entry由于左孩子右孩子本来就有就不需要构造,直接插入被插入页面,然后在原页面删除即可。
-
最后插入的entry,取出key更新parentEntry,然后插入parent页面。代码实现如下。
-
public void stealFromLeftInternalPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreeInternalPage page, BTreeInternalPage leftSibling, BTreeInternalPage parent, BTreeEntry parentEntry) throws DbException, TransactionAbortedException { // some code goes here // Move some of the entries from the left sibling to the page so // that the entries are evenly distributed. Be sure to update // the corresponding parent entry. Be sure to update the parent // pointers of all children in the entries that were moved. Iterator<BTreeEntry> moveEntry = leftSibling.reverseIterator(); int numSteal = (leftSibling.getNumEntries() - page.getNumEntries())/2; //将parent的entry移动到pagefindLeafPage BTreeEntry move = moveEntry.next(); BTreeEntry center = new BTreeEntry(parentEntry.getKey(), move.getRightChild(), page.iterator().next().getLeftChild()); page.insertEntry(center); //将sibling的entry取出,插入page for(int i=0; i<numSteal-1; ++i) { leftSibling.deleteKeyAndRightChild(move); page.insertEntry(move); move = moveEntry.next(); } leftSibling.deleteKeyAndRightChild(move); parentEntry.setKey(move.getKey()); parent.updateEntry(parentEntry); updateParentPointers(tid, dirtypages, page); }
-
同理,当非叶子节点插入页面在左边的时候,也是一样的原理,只不过左右区分一下就好了。
-
public void stealFromRightInternalPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreeInternalPage page, BTreeInternalPage rightSibling, BTreeInternalPage parent, BTreeEntry parentEntry) throws DbException, TransactionAbortedException { // some code goes here // Move some of the entries from the right sibling to the page so // that the entries are evenly distributed. Be sure to update // the corresponding parent entry. Be sure to update the parent // pointers of all children in the entries that were moved. Iterator<BTreeEntry> moveEntry = rightSibling.iterator(); int numSteal = (rightSibling.getNumEntries() - page.getNumEntries())/2; //将parent的entry移动到page BTreeEntry move = moveEntry.next(); BTreeEntry center = new BTreeEntry(parentEntry.getKey(), page.reverseIterator().next().getRightChild(), move.getLeftChild()); page.insertEntry(center); for(int i=0; i<numSteal-1; ++i) { rightSibling.deleteKeyAndLeftChild(move); page.insertEntry(move); move = moveEntry.next(); } rightSibling.deleteKeyAndLeftChild(move); parentEntry.setKey(move.getKey()); parent.updateEntry(parentEntry); updateParentPointers(tid, dirtypages, page); }
Exercise 4 合并页面的实现
-
首先是叶子节点的合并,这里统一是将右边的tuple合并到左边,同时记得更新左边页面的右邻居。下面是代码实现。
-
public void mergeLeafPages(TransactionId tid, Map<PageId, Page> dirtypages, BTreeLeafPage leftPage, BTreeLeafPage rightPage, BTreeInternalPage parent, BTreeEntry parentEntry) throws DbException, IOException, TransactionAbortedException { // some code goes here // // Move all the tuples from the right page to the left page, update // the sibling pointers, and make the right page available for reuse. // Delete the entry in the parent corresponding to the two pages that are merging - // deleteParentEntry() will be useful here Iterator<Tuple> tuples = rightPage.iterator(); int numTuples = rightPage.getNumTuples(); for(int i = 0; i < numTuples; i++) { Tuple tuple = tuples.next(); rightPage.deleteTuple(tuple); leftPage.insertTuple(tuple); } if(rightPage.getRightSiblingId() != null) { BTreeLeafPage rightSibling = (BTreeLeafPage) getPage(tid, dirtypages, rightPage.getRightSiblingId(), Permissions.READ_WRITE); rightSibling.setLeftSiblingId(leftPage.getId()); } leftPage.setRightSiblingId(rightPage.getRightSiblingId()); setEmptyPage(tid, dirtypages, rightPage.getId().getPageNumber()); deleteParentEntry(tid, dirtypages, leftPage, parent, parentEntry); }
-
非叶子节点的合并也很简单,就是把父节点拉下来构成一个entry,这个entry key是父节点的值,leftChild就是左边页面中的最后一个entry的右孩子,rightChild就是右边页面中的第一个entry的左孩子。然后把它插入左边页面,右边页面的也按照顺序插入即可。
-
public void mergeInternalPages(TransactionId tid, Map<PageId, Page> dirtypages, BTreeInternalPage leftPage, BTreeInternalPage rightPage, BTreeInternalPage parent, BTreeEntry parentEntry) throws DbException, IOException, TransactionAbortedException { // some code goes here // // Move all the entries from the right page to the left page, update // the parent pointers of the children in the entries that were moved, // and make the right page available for reuse // Delete the entry in the parent corresponding to the two pages that are merging - // deleteParentEntry() will be useful here Iterator<BTreeEntry> moveEntry = rightPage.iterator(); // 将父节点的索引节点插入leftPage中 BTreeEntry center = new BTreeEntry(parentEntry.getKey(), leftPage.reverseIterator().next().getRightChild(), rightPage.iterator().next().getLeftChild()); leftPage.insertEntry(center); // rightPage的entry插入leftpage里 while(moveEntry.hasNext()) { BTreeEntry entry = moveEntry.next(); rightPage.deleteKeyAndLeftChild(entry); leftPage.insertEntry(entry); } setEmptyPage(tid, dirtypages, rightPage.getId().getPageNumber()); //更新插入子页的对应父页 updateParentPointers(tid, dirtypages, leftPage); deleteParentEntry(tid, dirtypages, leftPage, parent, parentEntry); }
参考文章:
https://blog.csdn.net/hjw199666/category_9588041.html 特别鸣谢hjw199666 在我完成6.830的道路上给了很多代码指导,我的很多代码都是基于他的改的
https://www.zhihu.com/people/zhi-yue-zhang-42/posts