相信你对数据的索引并不陌生,最常见的索引结构是 B+Tree,索引可以加快数据库的检索速度,能极大地减少存储引擎需要扫描的数据量。但是你知道为什么用了索引之后,查询就会变快?B+ Tree 的结构原理是什么?8月25日 19:30 实战教程第三期 OceanBase 社区将带领你学习数据库索引结构,从基础的数据结构知识介绍,到 MiniOB 项目索引结构的实现,带你深入理解数据库是以什么样的方式加快对数据的检索。
除此之外,本期课程还将继续学习 OceanBase 存储引擎结构,带你了解 OceanBase 的数据分层结构和存储格式、以及数据在 OceanBase 中从插入到查询的整个过程,加深对 OceanBase 存储结构的理解。
本期能帮你解决什么问题?
1、为什么大部分的数据库存储引擎都选择使用 B+Tree 索引结构?
2、以 OceanBase 为例,深入学习一条数据从写入到查出在数据库中的的执行流程。
3、练习题没思路?OceanBase 技术专家在线为你解析 MiniOB Drop Table 实现思路。
直播内容抢“鲜”知
MiniOB B+Tree 实现
在基本的逻辑上,MiniOB B+Tree 和 B+Tree 是一致的,查询和插入都是从根逐层定位到叶结点,然后在叶结点内获取或者插入,插入过程导致叶结点满的话同样会分裂,并向上递归这一过程。
每个结点组织成一个固定大小的 page,每个 page 首先有一个 page_num 表示 page 在文件中的序号,然后每个结点 page 都有一个 common header,实现为 IndexNode 结构,包括 is_leaf(是否为叶结点),key_num(结点中key 的个数),parent(结点父结点的 page num),当 parent=-1 时表示该结点没有父结点。除此之外,Leaf page 还有 prev_brother(左结点的 page num)和 next_brother(右结点的 page num),这两项用于帮助遍历。最后 page 所剩下的空间就顺序存放键值对,叶结点所存放的键是索引列的值加上 RID,RID 是该行数据在磁盘上的位置,值则是 RID。也就是说有效键值数据都是直接存放在叶结点上的。
内部结点和叶结点有两点不同,一个是没有左右结点的 page num,另一个是所存放的值是 page num,即标识了子结点的 page 位置。键值对在内部结点有如上图的两种组织形式,每一层最左边结点的第一个键值对中没有存储键。
所有的结点,即 page 都存储在外存的索引文件 IndexFile 中,其中文件的第一个 page 是索引文件头,存储了一些元数据,如 root page 的 page num、内部结点和叶子结点能够存储键值对的最大个数等。
一个简单的 MiniOB B+Tree 将如上图所示,其中叶结点能够访问到左右叶结点,并且每个结点都能够访问到父结点。我们能够从 IndexFile 的第一个 page 得到 root page,而在知道一棵 B+Tree 的 root page 以后就能够访问到任意一个结点了。查询时我们会从 root page 开始逐层向下定位到目标叶结点,在每个 page 内遍历搜索查找键。
在插入时,我们首先定位到叶结点,如图中的 page2,然后在结点内定位一个插入位置,如果结点未满那么将键值对插入指定位置并向后移动部分数据即可。而如果结点已满,那么需要对其进行分裂,我们将先创建一个新的右兄弟结点,即 page5,然后在原结点内保留前一半的键值对,剩余的键值对则移动到新结点,并修改 page2 的后向 page num,page5 的前后向 page num 以及 page4 的前向 page num,再根据之前定位的插入位置判断是插入 page2 还是 page5 ,并完成叶结点的插入。此外,由于我们新增了结点,我们需要在父结点也插入新的键值对,这一步将涉及到原结点,新结点以及新结点中的最小键,分为两种情况:
1、有父结点,那么直接将新结点中的最小键以及新结点的 page num 作为键值对插入父结点即可。
2、假设此时没有父结点,那么我们将创建一个新的根结点,除了把新结点键值对插入,还会将原结点的 page num 作为第一个键值对的值进行插入。
如果父结点的键值对插入同样触发了分裂,我们将按上述的步骤递归执行。
正常的删除操作我们就不再介绍,这里我们介绍一下一些涉及结点合并的特殊情况。
删除时我们会首先在结点内删除键值对,然后判断其中的键值对数目是否小于一半,如果是则需要进行特殊处理。这里我们不特指叶结点,而描述一种普适的情况。
首先我们通过父结点找到该结点的左兄弟,如果是最左边的结点,则找到其右兄弟。
如果两个结点的所有键值对能容纳在一个结点内,那么进行合并操作,将右结点的数据迁移到左结点,并删除父结点中指向右结点的键值对。
如果两个结点的所有键值对不能容纳在一个结点内,那么进行重构操作,当所删除键值对的当前结点不是第一个结点时,我们选择将左兄弟的最后一个键值对移动到当前结点,并修改父结点中指向当前结点的键,而在当前结点是第一个结点时,我们选择将右兄弟的第一个键值对移动到当前结点。
在上述两种操作中,合并操作会导致父结点删除键值对,因此会向上递归地去判断是否需要再次的合并与重构。
存储格式概述
存储分层结构
在 OceanBase 数据库的存储视角看,存储结构里最上层是 Partition Group,对应一个分区组,很多时候我们也将其简称为 PG。同一个 PG 中的多个 Partition 始终绑定在一起,那么对于同一个 PG 的事务操作就会被优化为单机事务,写入性能会更好。每个 Partition 包含与之对应的多个版本的 Table Store,每个Table Store 包含特定时间数据快照 sstable。当历史版本的 Table Store 没有被访问时,会通过 GC 线程回收掉。
Table Store内部,数据按照热、温、冷的顺序,依次分布在 MemTable、Mini/Minor SSTable、Major SSTable中。最新数据写入 Active MemTable,当 MemTable 达到一定大小后,会冻结不再写入,创建新的写入MemTable。为了节省内存,冻结 MemTable 会转储写到磁盘文件上,生成 Mini SSTable。当 Mini SSTable 个数增多时,各个 SSTable 间主键交叉的概率会增大,影响查询效率,这时会触发 Mini/Minor SSTable 之间的合并,将多个多版本 SSTable合 并成 Minor SSTable。
OceanBase 采用 LSM Tree 的数据组织形式,数据只能以追加的方式写入。每次插入、更新、删除都是一条写入记录,MemTable 和 Mini/Minor SSTable 保留了数据的多版本信息,包含比较多的冗余信息。而现实业务中,一般比较少更新历史数据,并且只需要读取数据的最新版本,这样对于历史冷数据存储,有比较多的优化方案。OceanBase 系统运行时,在满足一定条件时,会选取某个历史快照点,读取数据最新版本,写入 Major SSTable中。Major SSTable 只保留主键在快照中的最新完整行,采用了行列混存的模式,对数据进行分段编码和压缩,减少数据存储成本的同时极大地提高了查询性能。
内存数据格式
OceanBase数据库的内存存储引擎 MemTable 由 BTree 和 Hashtable 组成,在 HashTable 和 BTree 中存储的均为指向对应数据的指针。
Hash Table 比较适合按主键查找,适用于 dml 时主键校验或者其它按主键查找;BTree 中数据都是按主键有序的,比较适合按主键范围查找。
磁盘文件格式
OceanBase 的磁盘存储文件叫作 SSTable,分为多版本 SSTable 和基线 SSTable。Mini SSTable 和 Minor SSTable都是多版本 SSTable,它包含一段连续时间内写入的数据;Major SSTable 是基线 SSTable,它包含某个快照点内的所有完整提交数据。为了兼顾写和读的性能,SSTable 内部又按数据大小分为宏块和微块。宏块是数据写 IO 的基本单位,大小为 2M 定长数据块;微块是数据读 IO 的基本单位,为变长数据块,微块内部数据可以按照行存或者列式编码存储,每个宏块包含多个微块,如下图所示。
在宏块的最前面的是宏块头,记录宏块内部微块个数,微块数据起始位置等信息;后面跟着的就是一个个长度不固定的微块,存储用户数据;在微块之后,存储微块索引信息(Micro Block Index),记录每个微块在宏块内的相对偏移 Offset、每个微块的 EndRowKey 等信息。
更多详细内容,敬请关注 8月25日 19:30 「从 0 到 1 数据库内核实战教程」官方课程。
附录:
内核实战教程第一期 | 成为内核开发者的第一步:搭建研发环境
内核实战教程第二期|带你揭开数据库存储结构的神秘面纱
课程回放
赶快扫描下方二维码进入「OceanBase 入门到实战」群
关注课程动态,和更多小伙伴一起学习进步
为帮助大家更好的学习数据库知识,结交新的朋友
未来 OceanBase Meetup 也会走到更多的城市中
大家进群后修改自己的群昵称哦【格式:姓名-城市-职位