Lab5主要包括4个exercise
exercise1 :实现B+树的搜索,根据给定的key查找适当的页节点。
exercise2 :实现内部节点、页节点的拆分,当页面中key的数量大于n-1时,对页面进行拆分。
exercise3 :实现节点的重新分配,当删除key后如果页面中key的数量小于m/2 时,从其兄弟节点“窃取”一个key
exercise4:实现节点的合并,当删除key后如果页面中key的数量小于m/2 时,且兄弟节点也只有m/2个key,则将两个节点合并。
B+树
B+树是B-树的变体,也是一颗多路搜索树。一棵n阶的B+树主要有这些特点:
- 每个结点至多有n个子女;
- 非根节点关键值个数范围:n/2 <= k <= n-1
- 相邻叶子节点是通过指针连起来的,并且是关键字大小排序的。
一颗3阶的B+树如下:
- B+树内部节点是不保存数据的,只作索引作用,它的叶子节点才保存数据。
- B+树相邻的叶子节点之间是通过链表指针连起来的
- B+树中,内部节点与其父节点的key值不能重复,页节点与其父节点的key值可以重复
B+树的查询
B+树的数据只存储在叶子节点,内部节点值存储索引,通过索引找到相应的叶子节点进行查询
B+树的单值查询
当查询key=70的节点时,首先从读取根节点,判断得key<75;然后读取根节点的左孩子节点,将70依次与左孩子节点中的值进行比较,判断得key>66;则读取66的右孩子节点,key存储于该叶节点中,读取其中的数据。
B+树的范围查询
当要读取[68,100]范围内的数据时,首先找到第一个大于等于68的节点,然后在叶节点中向后遍历。
B+树的插入
B+树的插入是从叶子节点中进行的,找到要插入的叶子结点,将数据插入。
然后根据插入后叶节点中key的数量进行不同的处理。
分裂叶节点和分裂内节点的情况是不同的。分裂叶节点时,节点中的key值复制到父节点中(即叶节点和内部节点可以有相同的值)
分裂内部节点时,是将节点中的key值“挤到”父节点中(即内部节点之间的key值不能重复)
B+树的删除
设
kyesNum = 节点中key的数量
min = ⌈n/2⌉-1 节点能容纳key的最小值,keysNum<min时该节点要进行合并或者“窃取”的操作。
max = n-1
7阶B+树,min = 3
叶子节点
keysNum < min ,兄弟节点的keys > min,从兄弟节点处“窃取”key
keysNum < min ,兄弟节点的keys = min,与兄弟节点合并
内部节点
keysNum < min ,兄弟节点的keys > min,从兄弟节点处“窃取”key
keysNum < min ,兄弟节点的keys = min,与兄弟节点合并
实验概述
实现B+树索引的查询、节点分裂、兄弟节点中元素的重新分配、兄弟节点的合并。
- 根据B+树的特性去查找所需元素。(exercise1的内容)
- 插入元素时会出现分裂节点的情况,实现内部节点和叶子节点的分裂。(exercise2的内容)
- 删除元素时根据兄弟节点的情况会进行元素的重新分配、兄弟节点的合并。实现内部节点和叶子节点的重新分配、合并。(exercise3、4的内容)
通过Lab提供的辅助类实现上述功能。
辅助类:
BTreePageId
:BTreeInternalPage、 BTreeLeafPage、 BTreeHeaderPage、BTreeRootPtrPage的唯一标识符,主要有三个属性- tableid:该page所在table的id。
- pgNo:该page所在page的序号(table中的第几个页)。
- pgcateg:用于标识BTreePage的类型。
BTreeInternalPage
:B+树的内部节点byte[] header;
:记录slot的占用情况Field[] keys;
:存储key的数组int[] children;
:存储page的序号,用于获取左孩子、右孩子的BTreePageIdint numSlots;
:内部节点中能存储的指针的数量(即n,内部节点中最多能存储key的数量为n-1)int childCategory;
:孩子节点的类型(内部节点或叶节点)
BTreeLeafPage
:B+树的叶节点byte[] header;
:记录slot的占用情况Tuple[] tuples;
:存储tuple的数组int numSlots;
:叶节点中能存储的tuple数量(即n-1)int leftSibling;
:左兄弟的pageNo,用于获取左兄弟的BTreePageId,为0则没有左兄弟int rightSibling;
:右兄弟的pageNo,用于获取右兄弟的BTreePageId,为0则没有右兄弟
BTreeEntry
:内部节点中的entry,内部节点对key的查找、插入、删除、迭代,都是以entry为单位的。Field key;
:内部节点中的keyBTreePageId leftChild;
:左孩子的BTreePageIdBTreePageId rightChild;
:右孩子的BTreePageIdRecordId rid;
:标识该entry所在的位置。(即该entry是哪个page中的)
BTreeInternalPage底层页面并不存储BTreeEntry,而是存储n-1个key和n个指向子节点的指针(子页的pageNo,父页与子页同处一个table中,知道子页的pageNo就能获取到子页)。
exercise 1
实现BTreeFile中的findLeafPage()方法,实现B+树的查询。
BTreeLeafPage findLeafPage
:递归函数,在B+树中查找可能包含字段 f 的叶节点。它使用只读权限锁定叶节点路径上的所有内部节点,并使用权限perm锁定叶节点。如果f为null,它将查找最左边的叶节点,用于迭代器。
private BTreeLeafPage findLeafPage(TransactionId tid, Map<PageId, Page> dirtypages, BTreePageId pid, Permissions perm,
Field f)
throws DbException, TransactionAbortedException {
// some code goes here
int type = pid.pgcateg();
if(type == BTreePageId.LEAF){
return (BTreeLeafPage) getPage(tid,dirtypages,pid,perm);
}
BTreeInternalPage internalPage = (BTreeInternalPage) getPage(tid,dirtypages,pid,Permissions.READ_ONLY);
Iterator<BTreeEntry> it = internalPage.iterator();
BTreeEntry entry = null;
while (it.hasNext()){
entry = it.next();
if(f == null){
return findLeafPage(tid,dirtypages,entry.getLeftChild(),perm,f);
}
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);
}
当前节点如果是叶子节点,则返回pid对应的BTreeLeafPage。否则向下递归找到 f 所在的叶子节点。当 f = null 时,返回最左侧的叶子结点。
参数:
Map<PageId, Page> dirtypages
:当创建新page或更改page中的数据、指针时,需要将其添加到dirtypages中
方法:
getPage
:调用getPage()获取page时,将检查页面是否已经存储在本地缓存dirtypage
中,如果不再,则调用BufferPool.getPage去获取。getPage()如果使用读写权限获取页面,也会将页面添加到dirtypages缓存中,因为它们可能很快就会被弄脏。这种方法的一个优点是,如果在一个元组插入或删除过程中多次访问相同的页面,它可以防止更新丢失。
exercise 2
实现splitLeafPage()和splitInternalPage()方法。
1、 splitLeafPage()
当页节点中元组数量等于n时,将其拆分成两个页节点。返回插入tuple所在的page
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..
//1、
BTreeLeafPage newRightPage = (BTreeLeafPage) getEmptyPage(tid,dirtypages,BTreePageId.LEAF);
//2、
int tuplesNum = page.getNumTuples();
Iterator<Tuple> it = page.reverseIterator();
for (int i=0; i<tuplesNum / 2; i++){
Tuple tuple = it.next();
page.deleteTuple(tuple);
newRightPage.insertTuple(tuple);
}
//3、
BTreePageId oldRightPageId = page.getRightSiblingId();
BTreeLeafPage oldRightPage = oldRightPageId == null ? null : (BTreeLeafPage) getPage(tid,dirtypages,oldRightPageId,Permissions.READ_ONLY);
if(oldRightPage != null){
oldRightPage.setLeftSiblingId(newRightPage.getId());
newRightPage.setRightSiblingId(oldRightPageId);
dirtypages.put(oldRightPageId,oldRightPage);
}
//4、
page.setRightSiblingId(newRightPage.getId());
newRightPage.setLeftSiblingId(page.getId());
dirtypages.put(page.getId(),page);
dirtypages.put(newRightPage.getId(),newRightPage);
//5、
BTreeInternalPage parent = getParentWithEmptySlots(tid,dirtypages,page.getParentId(),field);
Field mid = newRightPage.iterator().next().getField(keyField);
BTreeEntry entry = new BTreeEntry(mid,page.getId(),newRightPage.getId());
parent.insertEntry(entry);
dirtypages.put(parent.getId(),parent);
//6、
//将page、newRightPage的父指针设置为parent
updateParentPointers(tid, dirtypages, parent);
//7、
if(field.compare(Op.GREATER_THAN_OR_EQ,mid)){
return newRightPage;
}
return page;
}
1、通过getEmptyPage
创建一个newRightPage,
2、将当前page中一半的tuple插入到newRightPage中。插入时应该先从page中删除tuple,然后再插入到newRightPage。(newRightPage插入tuple后会给其赋值新的recordId,page删除tuple时根据其recordId进行查找然后删除,而page无法定位到被赋值了新recordId的tuple,则无法将其删除)。
3、如果当前page有右兄弟oldRightPage,将oldRightPage左兄弟的指针指向newRightPage,将newRightPage的右兄弟指针指向oldRightPage。并将oldRightPage添加到dirtypages中。
4、将page的右兄弟指针指向newRightPage,newRightPage的左兄弟指针指向page。将page、newRightPage添加到dirtypages中。
5、获取指向该page的内部节点,在其中添加一个指向page和newRightPage的新entry。将父entry所在的page添加到dirtypages中。
6、更新page、newRightPage的父指针。
7、返回field所在的页(page或newRightPage)。
方法:
getParentWithEmptySlots
:获取具有读写权限的父页面,如果父节点中key的数量到达了n-1,则会调用splitInternalPage()
方法向上递归,总之最终会返回一个可以插入新key的内部节点。
2、 splitInternalPage()
实现与splitLeafPage类似,区别是分裂之后要将中间的key“挤到” 父节点中去。
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.
//1、
BTreeInternalPage newRightPage = (BTreeInternalPage) getEmptyPage(tid,dirtypages,BTreePageId.INTERNAL);
Iterator<BTreeEntry> it = page.reverseIterator();
//2、
int tuplesNum = page.getNumEntries();
for(int i=0; i<tuplesNum / 2; i++){
BTreeEntry entry = it.next();
page.deleteKeyAndRightChild(entry);
newRightPage.insertEntry(entry); //当entry被添加到newRightPage之后它的recordId被更改了,再在page中删除,是找不到这个entry的
//所以只能先删除再插入到新的Page中
}
//3、
BTreeEntry mid = it.next();
page.deleteKeyAndRightChild(mid);
mid.setLeftChild(page.getId());
mid.setRightChild(newRightPage.getId());
BTreeInternalPage parent = getParentWithEmptySlots(tid,dirtypages,page.getParentId(),mid.getKey());
parent.insertEntry(mid);
//4、
dirtypages.put(parent.getId(), parent);
dirtypages.put(page.getId(), page);
dirtypages.put(newRightPage.getId(),newRightPage);
updateParentPointers(tid, dirtypages, parent);
updateParentPointers(tid,dirtypages,page);
updateParentPointers(tid,dirtypages,newRightPage);
//5、
if(field.compare(Op.GREATER_THAN_OR_EQ, mid.getKey())){
return newRightPage;
}
return page;
}
deleteKeyAndRightChild
:
1、通过getEmptyPage
创建一个newRightPage。
2、将当前page中一半的entry插入到newRightPage中。同样,先从page中删除entry,再将其插入到newRightPage中。
3、分配完entry后,选出page中最大的entry,将其从page中删除,并将该entry的左孩子指针指向page,右孩子指针指向newRightPage,获取父节点parent,将该entry添加到父节点中(实现将中间的key“挤到”父节点中)。
4、将父节点parent、page、newRightPage添加到dirtypages中,并更新它们孩子节点的父指针。
5、返回field所在的页(page或newRightPage)。
exercise 3
实现stealFromLeafPage()、stealFromLeftInternalPage()、stealFromRightInternalPage()方法。
尝试从小于半满的叶页面中删除元组会导致该页面从其兄弟之一窃取元素或与其兄弟之一合并。exercise3中就需要实现叶节点和内部节点从兄弟节点“窃取”元素的功能。
1、stealFromLeafPage()
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.
//1、
Iterator<Tuple> it = isRightSibling ? sibling.iterator() : sibling.reverseIterator();
//2、
int curTuplesNum = page.getNumTuples();
int siblingTuplesNum = sibling.getNumTuples();
int targetTuplesNum = (curTuplesNum + siblingTuplesNum) / 2;
while(curTuplesNum < targetTuplesNum){
Tuple tuple = it.next();
sibling.deleteTuple(tuple);
page.insertTuple(tuple);
curTuplesNum++;
}
//3、
Tuple mid = it.next();
entry.setKey(mid.getField(keyField));
parent.updateEntry(entry);
return;
}
1、根据传入的参数isRightSibling
确定是从左兄弟中“窃取”,还是从右兄弟中“窃取”。
2、根据兄弟节点中tuple的数量,确定“窃取的数量”。
3、参数entry
是父节点中指向page和其兄弟节点的entry,将entry
的key更改为page和其兄弟节点key的中间值。
2、stealFromLeftInternalPage()
平均分配key后,将中间的key“挤到”父节点中去。
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.
//1、
Iterator<BTreeEntry> it = leftSibling.reverseIterator();
int curEntriesNum = page.getNumEntries();
int siblingEntriesNum = leftSibling.getNumEntries();
int targetEntriesNum = (curEntriesNum + siblingEntriesNum) / 2;
//2、
BTreeEntry entry = it.next();
BTreeEntry mid = new BTreeEntry(parentEntry.getKey(),entry.getRightChild(),page.iterator().next().getLeftChild());
page.insertEntry(mid);
curEntriesNum++;
//3、
while(curEntriesNum < targetEntriesNum){
leftSibling.deleteKeyAndRightChild(entry);
page.insertEntry(entry);
curEntriesNum++;
entry = it.next();
}
//4、
leftSibling.deleteKeyAndRightChild(entry);
parentEntry.setKey(entry.getKey());
parent.updateEntry(parentEntry);
//5、
dirtypages.put(page.getId(),page);
dirtypages.put(leftSibling.getId(),leftSibling);
dirtypages.put(parent.getId(),parent);
updateParentPointers(tid,dirtypages,page);
}
1、根据page及其左兄弟中key的数量,确定从其做兄弟中“窃取”几个key。
2、因为内部节点与其父节点中的key值没有重复,迁移key的时候也需要将父节点中的key移动到page中。
3、将page左兄弟节点中的key平均分配。
4、分配之后,将page左兄弟节点中最大的key“挤到”父节点中。
5、更新更新page与其左兄弟的父指针。
3、stealFromRightInternalPage()
实现与stealFromLeftInternalPage
相同
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> it = rightSibling.iterator();
int curEntriesNum = page.getNumEntries();
int siblingEntriesNum = rightSibling.getNumEntries();
int targetEntriesNum = (curEntriesNum + siblingEntriesNum) / 2;
BTreeEntry entry = it.next();
BTreeEntry mid = new BTreeEntry(parentEntry.getKey(), page.reverseIterator().next().getRightChild(), entry.getLeftChild());
page.insertEntry(mid);
curEntriesNum++;
while(curEntriesNum < targetEntriesNum){
rightSibling.deleteKeyAndLeftChild(entry);
page.insertEntry(entry);
entry = it.next();
curEntriesNum++;
}
rightSibling.deleteKeyAndLeftChild(entry);
parentEntry.setKey(entry.getKey());
parent.updateEntry(parentEntry);
dirtypages.put(page.getId(),page);
dirtypages.put(rightSibling.getId(),rightSibling);
dirtypages.put(parent.getId(),parent);
updateParentPointers(tid,dirtypages,page);
}
exercise4
实现mergeLeafPages()和mergeInternalPages()
删除元组时如果同级也处于最小占用率,则两个页面应合并,并从父级中删除指向两个page的entry
1、 mergeLeafPages()
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
//1、
Iterator<Tuple> it = rightPage.iterator();
while(it.hasNext()){
Tuple tuple = it.next();
rightPage.deleteTuple(tuple);
leftPage.insertTuple(tuple);
}
//2、
BTreePageId rightPageRightSiblingId = rightPage.getRightSiblingId();
if(rightPageRightSiblingId == null){
leftPage.setRightSiblingId(null);
}
else{
leftPage.setRightSiblingId(rightPageRightSiblingId);
BTreeLeafPage rightPageRightSibling = (BTreeLeafPage) getPage(tid,dirtypages,rightPageRightSiblingId,Permissions.READ_WRITE);
rightPageRightSibling.setLeftSiblingId(leftPage.getId());
}
//3、
setEmptyPage(tid, dirtypages, rightPage.pid.getPageNumber()); //将rightPage在header处置空
//4、
deleteParentEntry(tid, dirtypages, leftPage, parent, parentEntry);
//5、
dirtypages.put(leftPage.getId(),leftPage);
dirtypages.put(parent.getId(),parent);
}
1、将rightPage中的所有tuple添加到leftPage中。
2、判断rightPage是否有右兄弟,如果没有leftPage的右兄弟为空,如果有leftPage的右兄弟指向rightPage的右兄弟。
3、调用setEmptyPage
方法将rightPage在header标记为空。
4、调用deleteParentEntry
方法,从父级中删除左右孩子指针指向leftPage和rightPage的entry。
5、将leftPage与parent添加到dirtypages中
2、mergeInternalPages()
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
//1、
BTreeEntry mid = new BTreeEntry(parentEntry.getKey(),leftPage.reverseIterator().next().getRightChild(),rightPage.iterator().next().getLeftChild());
leftPage.insertEntry(mid);
//2、
Iterator<BTreeEntry> rightIt = rightPage.iterator();
while (rightIt.hasNext()){
BTreeEntry entry = rightIt.next();
rightPage.deleteKeyAndLeftChild(entry);
leftPage.insertEntry(entry);
}
//3、
updateParentPointers(tid,dirtypages,leftPage);
//4、
setEmptyPage(tid,dirtypages,rightPage.getId().getPageNumber());
//5、
deleteParentEntry(tid,dirtypages,leftPage,parent,parentEntry);
//6、
dirtypages.put(leftPage.getId(),leftPage);
dirtypages.put(parent.getId(),parent);
}
1、先将父节点中的指向leftPage和rightPage的entry添加到leftPage中
2、将rightPage中的entry添加到leftPage中
3、更新leftPage孩子节点的指针(将原本父节点指向rightPage的孩子节点的父节点更新为leftPage)
4、调用setEmptyPage
方法将rightPage在header标记为空。
5、调用deleteParentEntry
方法,从父级中删除左右孩子指针指向leftPage和rightPage的entry。
6、将leftPage与parent添加到dirtypages中