B+树的具体体现在于非叶子节点,用于标识范围并指向叶子节点页,也就是说,通过某个索引,我们无法获得具体的行位置,只能获得行所在的叶子页。所以非叶节点就是B+树的非叶子页,用于范围索引,而叶子页就是B+树的叶子页,用于存储数据。
我们在定位到了叶子页后,我们还需要检索叶子页的数据,这个检索需要由页目录来进行二分查找来找到具体的行数据。同时由于非叶节点页也有页目录,我们也可以通过页目录来快速的定位到某个节点而不是从根节点进行遍历进行判断,从而加快找到某个大范围。
这儿的大范围指的是将索引节点又划分了一次范围。比如索引节点有以下几个值:
infimum 4 8 12 16 20 24 28 32 36 40 supremum
按照正常的B+树索引查找,我们是从infimum进行顺序查找来查找到key是否落在索引节点的范围内。为了快速进行查找,innodb的索引页中都有页目录,在这儿生成的页目录为:
infimum(1) 12(3) 28(4) supremum(4)
后面括号内的值为该槽所拥有的记录数,未标明的也就是0,不存在于页目录中,假设存在于页目录中,其值必定大于0。也就是我们假如将括号内的值相加,获得的值,正好是索引节点数,也就是12个。
我们在查找页目录的时候,主要是查找范围,假设我们要查找的某个值落在了某个页目录之前并大于之前的目录值,那么我们就可以从该目录指向的值和它之前的拥有记录数-1个记录进行查找。
举个例子,假设在这儿,我们需要查找值24。我们通过以上的规则可以知道,24小于目录值28,该目录拥有4个索引记录(包括自己),同时24又大于12,所以理论上我们应该从28进行查找,同时查找28拥有的记录24、20、16。在这里进行查找就相当的简单了,和B+树索引的查找一样,大于等于当前节点值并且小于之后一个节点的值就满足当前条件,取当前节点指向的下一页。
可能很多人会纳闷,为什么查找目录槽是根据当前槽和之前槽之间的范围呢?这就牵扯到了页目录的分配。
首先,页目录是倒序并且有序的。初始会有2个页目录,一个是infimum,一个是supremum,它们因为都在页目录中,所以各自记录的owned值(拥有的记录数)都是1。因为页目录是稀疏索引,所以每个索引不是只是指向一条记录,而是指向多条记录,所以会有owned这个值。
当我们插入了一个值,比如1的时候,槽目录会发生改变:
infi(1) supremum(2)[1]
括号内为该槽拥有的记录数,而后面方括号内的是拥有的记录的值。我们可以看见,槽还是只有2个,但是supremum记录拥有的记录变成了2,也就是新的记录1依附到了supremum记录上了。每次新增记录,都会寻找离插入记录主键最接近并比它大的槽进行依附。
我们再次插入5个值,于是槽目录会变为:
infi(1) supremum(7)[1,2,3,4,5,6]
每个槽可以拥有的记录数是有限的,范围是4-8个,除去自己,也就是3-7个记录,这个拥有数是有依据的,当达到8个的情况下,可以进行槽的分裂。当然supremum记录是没有下限的,它的范围是1-8个。
我们再次插入一个值,分裂前应当是这样:
infi(1) supremum(8)[1,2,3,4,5,6,7]
但是这儿的拥有数已经到达了上限8,于是进行一次分裂:
infi(1) 4(4)[1,2,3] supremum(4)[5,6,7]
于是我们可以看见,槽进行了分裂,变为了3个,每个槽依旧拥有4-8个记录。
我们上面提到了,查找到某个槽之后,用该槽指向的记录和拥有数-1个之前的记录进行查找,但是这样在innodb内是不可行的。innodb索引页内所有的recorder通过单向链表升序排列,所以降序查找是没法查找的。所以在实际情况中,我们查找到的槽应当是查找到某个槽(slot1)之前的槽(slot0),但是那个槽指向的recorder不在查找范围内,查找的范围是slot0指向的recorder之后一条recorder开始,查找slot1拥有数条记录。
假设在这儿我们需要查找记录5,很容易知道该记录应当属于槽supremum,但是我们实际进行查找的记录应当是之前一个槽(指向4)的下一条记录(5)之后的4条,也就是5->6->7->supremum。其实这个和我们上述讲的忽略单向链表时候的操作记录是一致的,忽略单向链表直接查找supremum的话,查找的记录是supremum->7->6->5,实际上和调整后的查找范围是一致的。