B+树

一棵 m 阶的 B+ 树最多有 m - 1 个关键字,孩子结点的个数也就是阶数
上图就是一个 4 阶的 B+树,维基百科上的定义是关键字个数比孩子节点数小 1
构造
B+数是B树的变种,是一颗多叉搜索树,一颗 n 阶的B+树主要有一下特点:
- 每个节点至多有 n 个子女
- 非根节点关键值个数范围: n / 2 <= k <= n - 1
- 叶子节点间通过直接相连
- 非叶节点不保存数据,只做索引作用(可以储存更多的指针)
- 内部节点和父节点的 key 值不能重复,页节点与其父节点可以重复
查询
与 key 比较,逐层向下寻找
插入流程
-
将数据插入到叶子节点当中
-
判断叶子节点的 key 数量是否小于 n
小于
- 结束
大于
- 将该节点分裂成两个叶子节点
- 复制第( n / 2 )个key,将其添加到父节点(内部节点)中
-
判断父节点的 key 数量是否小于n
大于
- 分裂父节点
- 将第( n / 2 )个key,添加到父节点中
分裂叶子节点(具体的内容节点)
节点中的 key 值复制到父结点中(父节点和子节点可以重复)
- 抽出来的值相当于做一个索引,不储存相应的值
比如:
父节点 1
子节点 1 2 3 4 5 6
// 子节点此时候太多了需要分裂
父节点 1 4
子节点 1 2 3 4 5 6
分裂内部节点(也就是索引节点)
也就是说内部节点太多,需要挤压到父节点(父节点和子节点不能够重复)
- 原本就是索引,所以抽出来的也是索引列,可以从子节点中取出
父节点 1
子节点 2 3 4 5 6
--------------------------------
父节点 1 4
子节点 2 3 5 6
删除过程
规则:
子节点个数大于(B+树的阶数 / 2 - 1) Math.ceil(m-1)/2 – 1 向上取整
过程:
- 删除节点后判断是否符合规则
- 若兄弟节点 key 中有富余的key,向兄弟节点借一个key,同时借的key代替成为父节点的索引,否则第 3 步
- 若兄弟节点 key 中没有富余的key,则当前节点和兄弟节点合并成为一个新的节点,删除父节点的key,当前节点指向父节点(必为索引节点)
- 若索引节点的key的个数大于等于规则,结束,否则第 5 步
- 若兄弟节点有富余,父节点key下移,兄弟节点key上移,结束,否则第 6 步
- 当前节点和兄弟节点及父节点下移key合并成一个新的节点。当前节点指向父节点,重复第 4 步
PS:通过 B+ 树的删除操作后,索引节点存在的key,不一定在子节点当中存在相应的记录
前提概要
辅助类
BTreePageId:页面标识符
BTreeInternalPage、BTreeLeafPage、BTreeHeaderPage 和 BTreeRootPtrPage 对象的唯一标识符。
- private final int tableId; // 表id
- private final int pgNo; // 表内的页面id
- private final int pgcateg; // 页面的类型
- public final static int ROOT_PTR = 0;
- public final static int INTERNAL = 1;
- public final static int LEAF = 2;
- public final static int HEADER = 3;
BTreeInternalPage:B+树的内部节点
BTreeInternalPage 的每个实例都存储 BTreeFile 的一页的数据,并实现 BufferPool 使用的 Page 接口。
- private final byte[] header; //槽位占用的情况
- private final Field[] keys; // 记录key的数组
- private final int[] children; // 存储page的序号(PgNo),用于获取左孩子、右孩子的BTreePageId
- private final int numSlots; // 槽数量
- private int childCategory; // 孩子节点的类型:叶子或内部节点
BTreeLeafPage:B+树的叶节点
BTreeLeafPage 的每个实例都存储 BTreeFile 的一页的数据,并实现 BufferPool 使用的 Page 接口
- private final byte[] header; // 槽位占用的情况
- private final Tuple[] tuples; // 元组数组
- private final int numSlots; // 槽数量
- private int leftSibling; // 叶子节点(pgNo)或 0
- private int rightSibling; // 叶子节点(pgNo)或 0
BTreeEntry:
-
private Field key; // 内部节点中的key
-
private BTreePageId leftChild; // 左孩子
-
private BTreePageId rightChild; // 右孩子
-
private RecordId rid; // 储存在页面的路径
Exercise 1
实验一针对的是搜索
BTreeFile
参数
- private final File f; // 文件
- private final TupleDesc td; // 元组的模式
- private final int tableid ; // 表id
- private final int keyField; // 索引键所在的字段
方法
- findLeafPage:递归函数,查找并锁定 B+ 树中与可能包含关键字段 f 的最左侧页面相对应的叶子页面
Map<PageId, Page> dirtypages:当创建新page或更改page中的数据、指针时,需要将其添加到dirtypages中
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);
}
// 锁定路径上的所有内部节点,以READ_ONLY锁
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);
}
-
getPage()
封装锁取页面过程的方法。首先,该方法检查本地缓存(“dirtypages”),如果在那里找不到请求的页面,则从缓冲池中获取它。如果以读写权限获取页面,它还会将页面添加到dirtypages缓存中,因为据推测它们很快就会被此事务弄脏。
测试
BTreeFileReadTest.java and the system tests in BTreeScanTest.java.
Exercise 2
实验二针对的是新增节点
BTreeFile
参数
- private final File f; // 文件
- private final TupleDesc td; // 元组的模式
- private final int tableid ; // 表id
- private final int keyField; // 索引键所在的字段
方法
splitLeafPage():分裂叶子节点
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. 插入当前的tuple,分割一半节点给右节点
// 获取反向迭代器
int tupleNum = page.getNumTuples();
Iterator<Tuple> it = page.reverseIterator();
for (int i = 0; i < tupleNum / 2; i++) {
Tuple tuple = it.next();
// 原页面删除
page.deleteTuple(tuple);
// 写入新页面
newRightPage.insertTuple(tuple);
}
// 3. 如果当前 page 有右兄弟,连接右兄弟
BTreePageId oldRightPageId = page.getRightSiblingId();
// 获取页面
BTreeLeafPage oldRightPage = oldRightPageId == null ? null : (BTreeLeafPage) getPage(tid, dirtypages

本文详细介绍了B+树的构造原理,包括其节点类型和特性,以及查询、插入和删除操作的具体流程。在查询过程中,通过逐层比较key来定位目标叶子节点;插入时,当叶子节点满时会分裂,内部节点同样遵循此规则;删除时,通过借key或合并节点来保持结构平衡。此外,文章还提供了相关的辅助类和方法实现。
最低0.47元/天 解锁文章
1350

被折叠的 条评论
为什么被折叠?



