TableIndexHashTable前面已经介绍过了。在数据库系统中,可以作为sql执行时的临时数据结构,或者存储一些元数据信息,或者作为表的Hash索引,但是对于表索引,在更一般的场景下,B+树是更广泛的选择。表索引(Index)是指表中数据的一些子集。这些子集可以识别表中数据的一些特征,从而可以快速查找表中数据,数据库可以保证表中数据与表索引数据的一致性。B+树的基本概念B+树实际上是一种平衡树结构,可以保证数据的有序存储,可以在平均O(logn)复杂度下查询、插入、删除数据。它实际上类似于二叉平衡树,但每个节点可以有2个以上的子节点,并且可以适应数据库读取整块数据的需要,提高数据读写效率。B+树是一棵多叉平衡树。例如,M个叉子表示每个内层节点有M条到子节点的路径,具有以下基本特征:完全平衡的树结构,即所有子节点到根节点的距离总是一致的,除了根节点,每个节点的数据量至少大于M的一半,即M/2-1<=#keys<=M-1在每个有k个key的节点中,有k+1个指向child的指针nodes如下图所示,是B+树的一个例??子。最下面的节点称为叶节点,上面的称为内节点,最上面的内节点是根节点。实际数据并不存储在内部节点中,而是存储了指向其子节点的键和指针。叶子节点之间有指针串联,方便扫描操作;每个叶节点存储实际的键/值数据。叶子节点的内部结构一般有两种不同的布局方式。一种很常见,存储一个页面id,在节点中存储索引的key和value,然后一页指向下一页。这样把key/value保存在一起,不利于顺序扫描。如果我们只需要扫描key,就会有一些额外的消耗。另一种方式是key和value分离,结构大致如下:包含一些元数据信息,比如当前层数,可用空间等,以及一个有序的key列表,每个key指向它自己的价值。至于实际值所代表的数据,每个系统都有不同的实现,大致有两种:一种是存储一个记录id,指向数据在磁盘中页面的实际位置;另一种是直接存储数据本身。B+树操作插入B+树中插入操作的大致流程如下:首先从根节点开始,通过内节点指针逐层找到对应的叶子节点L,将数据存入叶节点。如果节点L中还有空闲空间,则操作完成,否则,分裂(split)叶子节点,重新分配叶子节点上的数据。以中间键为界,将右边的数据拆分成一个新的节点。分裂时需要将叶子节点中间的key推送到上层父节点,如果上层父节点需要再次分裂,重复上述过程。deletedelete操作与insert大致相反。插入时,如果某个节点上的数据已满,则需要拆分;在删除的时候,如果一个结点树中的数据小于M/2-1,那么B+树的定义就会被破坏。这时候需要对节点进行合并,称为merge。大致过程如下:通过内层节点node找到对应的叶子节点L,在L中找到对应的数据并删除。如果叶节点中的数据大于等于M/2-1,则完成。否则,需要合并合并节点尝试重新分配。如果相邻节点有剩余,则从相邻节点取一个key。如果相邻节点没有冗余数据,则尝试与兄弟节点合并。这里有一个动态展示B+树的插入、删除、查找过程的网站,可以更好的帮助大家理解B+树。https://www.cs.usfca.edu/~galles/visualization/BPlusTree.htmlB+树的设计接下来我们简单分析一下关于B+树的一些设计问题。NodeSize其实是不确定B+树中每个节点的大小,或者取决于硬件环境、数据库类型、sql查询类型等因素。一般来说,存储介质越慢,节点的容量就越大,原因很简单。在慢速介质中,比如磁盘,我们肯定希望一次能够读取更多的数据到内存中,尽量避免多次随机IO。在更快的设备上,节点的容量更小。可以参考内存中的缓存行和操作系统维护的页面大小。MySQL的B+树节点大小一般为16KB。MergeThreshold前面提到删除数据可能会触发B+树的合并节点操作,但在实际实现中,有些数据库系统并不是只要满足条件就立即执行,而是在一段时间后才进行合并。因为合并是一个非常“昂贵”的操作,涉及调整数据在磁盘上的分布,所以一些系统会有一些后台进程周期性地触发这个操作。Variable-LengthKeys上面提到的B+Tree存储了所有固定大小的key,但是在实际环境中,我们的key或者value可能是可变长度的。对于变长的key,一般有以下几种解决方案:1.节点中不存储实际的key,而是存储指向实际元组属性的指针。在这种情况下,虽然可以固定大小,但是指针不具备key本身的特性,范围扫描效率低下,在实践中使用不多。2.节点大小不固定。为了适应不同长度的key,这种方式需要更细化的内存管理,也需要在bufferpool中进行适配(bufferpool的大小通常是固定的),所以实际使用的并不多。3.对齐,无论key的大小如何,key总是与其类型的大小对齐。这种方式的缺点也很明显,就是可能会浪费一定的空间。4.使用类似于slotpage的组织,将key存放在一个有序的集合中,指向它的实际数据。非唯一索引数据库中的索引有时会存在重复数据,唯一索引除外。因此,B+Tree在存储数据时,需要对重复键进行处理。很容易理解,DuplicateKeys存储的是重复键,即我们可以将重复键存储为单个键。ValueList值列表为重复键的值维护一个链表,并将它们串联起来。B+树优化最后,让我们看一下B+树设计的一些优化方案。PrefixCompression由于B+树底部叶子节点的数据是有序排列的,所以同一个叶子节点中存储的数据很可能具有相同的特征,比如它们可能相似,具有相同的特征字首。因此,对于那些具有相同前缀的数据,可以只存储一个前缀数据的副本,而不是为每个键存储一个单独的副本。SuffixTruncation前面提到B+树中的内层节点只存储key和索引信息,不存储数据。它仅用作辅助搜索的索引节点。因此,内节点中的key不需要是一个完整的key,只要能起到区分搜索的作用即可。比如上面的例子,由于两个key的所有字符都不一致,所以不需要保存完整的key,只保存能帮助找到下一级节点的信息。用BulkInsert构建B+树最好的方法是用一组有序的数据自下而上构建B+树的多级索引,这样B+树就不会分裂或合并,而且效率是最高的。因此,如果一些初始化数据需要插入到B+树中,可以先排序,然后直接构建B+树。PointerSwizzlingB+树在遍历页面时,每次都需要从缓冲池中获取页面的位置信息,然后B+树根据这个位置获取BufferPool中的页面数据。比如上图中,如果需要获取pageid为2的页面,就会先从页表中获取。BufferPool判断页面是否在内存中。如果没有,则将页面加载到内存中,并返回指向该页面(页面实际位置)的指针给B+树。如果B+树确认扫描到的页一定在内存中,那么就可以直接存放页指针,省去了从页表寻址的开销。这种优化更适合B+树的上层节点,因为B+树上层的容量比较小,会被频繁访问,可以缓存在内存中,加快B+树的查询速度。Conclusion这部分描述了B+树的一些基本概念。相信读者可以对其有一个基本的了解。在大多数情况下,B+树是一种在数据库中被广泛使用的结构。
