MIT6.830 Lab 5 B+ Tree Index B+树索引

Lab 5 B+ Tree Index

本次lab就是要实现B+树索引,在lab 1中实现的HeapFile实际上实现的是顺序索引,但真正数据库使用的一般都是B+树索引,或者像leveldb使用跳表作为索引。如果需要补充B树或者B+树的知识可以看下面两个链接,B树B+树

Exercise 1 实现BTreeFile.findLeafPage()

  • 首先我们需要明确的是findLeafPage函数做的是什么,该函数提供的功能是,返回所需节点的所在页面,也就是找到该节点所在的叶子节点的页面。同时在函数内部实现递归,不断的向下查找到叶子节点页面。根据lab 5的实验文档,我们可以将查找节点的页面分为三种情况:

    1. 当传进来的页面就是叶子节点,那么就表示找到了所需节点的页面,直接调用getPage函数从bufferpool中获取该页面。
    2. 当寻找的节点为null时,则递归的寻找最左侧的叶子节点所对应的页面。
    3. 当传进来的页面是非叶子节点的页面且寻找的节点不为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进行分别分裂函数的实现。

  • 首先是关于叶子节点的分裂,分裂都是将部分节点移动到新的右叶子页面中。所以函数实现步骤如下:

    1. 创建新的叶子页面,将当前传入的需要分裂的叶子的反向迭代器取出。
    2. 将迭代器中的一半tuple插入,新的右叶子页面中。
    3. 之后判断需要分裂的叶子页面是否还有右邻居,如果有将右邻居的左指针(这里其实不一定是指针,可能是一个标志位),指向新的新的右叶子页面。
    4. 然后将新的右叶子页面的左节点指向分裂页面,将右节点指向旧的右页面。
    5. 最后将新的叶子节点的页面的第一个值作为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);
    
    
    	}
    
  • 其次就是关于非叶子节点的分裂,分裂还是将部分节点移动到新的右叶子页面。所以函数实现步骤如下:

    1. 创建新的非叶子节点,将当前传入的需要分裂的叶子的反向迭代器取出。
    2. 将迭代器的一半tuple插入,新的右叶子页面中。
    3. 然后将迭代器n/2的tuple取出,然后在分裂页面中将其删除,同时将其更新到父节点中,即创建新的Entry,插入到上层的BTreeInternalPage中。
    4. 在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+树底层插入的时候需要保持数据是有序的,所以会产生一下两种情况:

    1. 当被插入页面在左边的时候,那多出来的tuple的页面就在右边,那么我们就希望获取正向迭代器。这样的话,就在insertTuple函数中可以直接逐个插入在最后面,就不需要很多判断减少判断的耗时。
    2. 当被插入页面在右边的时候,那多出来的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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值