一种可以灵活管理所有目录项
的方式。他们灵光乍现,忽然发现这些目录项
其实长得跟我们的用户记录差不多,只不过目录项
中的两个列是主键
和页号
而已,所以他们复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,我们把这些用来表示目录项的记录称为目录项记录
。那InnoDB
怎么区分一条记录是普通的用户记录还是目录项记录
呢?别忘了记录头信息里的record_type
属性,它的各个取值代表的意思如下:
0
:普通的用户记录
1
:目录项记录
2
:最小记录
3
:最大记录
原来这个值为1
的record_type
是这个意思呀,我们把前边使用到的目录项放到数据页中的样子就是这样:
我们来说一下目录项记录
和用户记录
的区别:
目录项记录
的record_type
值是 1,而普通用户记录的record_type
值是 0。
目录项记录
只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB
自己添加的隐藏列。
除了上述几点外,这两者就没啥差别了,它们用的是一样的数据页,页的组成结构也是一样一样的(就是我们前边介绍过的 7 个部分),都会为主键值生成Page Directory
(页目录)以加快在页内的查询速度。
所以现在根据某个主键值去查找记录的步骤可以大致拆分成下边两步,以查找主键为20
的记录为例(因为都是从一个页中通过主键查某条记录,所以都可以使用Page Directory
通过二分法而实现快速查找):
- 先到存储
目录项记录
的页中通过二分法快速定位到对应目录项,因为12 < 20 < 209
,所以定位到对应的记录所在的页就是页9
。
- 从
页9
中根据二分法快速定位到主键值为20
的用户记录(这个过程不再多说)。
虽然说目录项记录中只是存储主键值和对应的页号,由于一个页中只有16KB的大小,能存放的目录项记录也是有限的,所以当一个页存储目录项满了之后再有的话就需要再来一个存储目录项记录的页。
为了大家更好的理解如何新分配一个目录项记录
页的过程,我们假设一个存储目录项记录
的页最多只能存放 4 条目录项记录
(请注意是假设哦,真实情况下可以存放好多条的),所以如果此时我们再向上图中插入一条主键值为320
的用户记录的话,那就需要一个分配一个新的存储目录项记录
的页喽:
以上图中,由于我们新增了一条记录,所以得到了一个新的数据页,里面存放的是数据记录,又因为页30的页目录记录存储满了(上面做了假设,假设每页最多只能存储4条),所以有了页32来存放页31对应的目录项。
因为存储目录项记录
的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要 3 个步骤:
- 确定
目录项记录
页;
- 我们现在的存储
目录项记录
的页有两个,即页30
和页32
,又因为页30
表示的目录项的主键值的范围是[1, 320)
,页32
表示的目录项的主键值不小于320
,所以主键值为20
的记录对应的目录项记录在页30
中。
- 通过
目录项记录
页确定用户记录真实所在的页;
- 在一个存储
目录项记录
中定位一条目录项记录的方式说过了(通过二分查找进行定位,找到对应的页)。
- 在真实存储用户记录的页中定位到具体的记录;
- 不多说了。
那么问题来了,在这个查询步骤的第 1 步中我们需要定位存储目录项记录
的页,但是这些页在存储空间中也可能不挨着,如果我们表中的数据非常多则会产生很多存储目录项记录
的页,那我们怎么根据主键值快速定位一个存储目录项记录
的页呢?
其实也简单,为这些存储目录项记录
的页再生成一个更高级的目录,就像是一个多级目录一样,大目录里嵌套小目录,小目录里才是实际的数据,所以现在各个页的示意图就是这样子:
如图,我们生成了一个存储更高级目录项的页33
,这个页中的两条记录分别代表页30
和页32
,如果用户记录的主键值在[1, 320)
之间,则到页30
中查找更详细的目录项记录
,如果主键值不小于320
的话,就到页32
中查找更详细的目录项记录
。
随着表中记录的增加,这个目录的层级会继续增加,如果简化一下,那么我们可以用下边这个图来描述它:
其实这是一种组织数据的形式,或者说是一种数据结构,它的名称是B+
树。
因为我们把数据页都存放到B+
树这个数据结构中了,所以我们也把我们的数据页称为节点
。从图中可以看出来,我们的实际用户记录其实都存放在 B + 树的最底层的节点上,这些节点也被称为叶子节点
或叶节点
,其余的节点都是用来存放目录项
的,这些节点统统被称为内节点
或者说非叶节点
。其中最上边的那个节点也称为根节点
。
从图中可以看出来,一个B+
树的节点其实可以分成好多层,设计InnoDB
的大叔们为了讨论方便,规定最下边的那层,也就是存放我们用户记录的那层为第0
层,之后依次往上加。上边我们做了一个非常极端的假设,存放用户记录的页最多存放 3 条记录,存放目录项记录的页最多存放 4 条记录,其实真实环境中一个页存放的记录数量是非常大的,假设,假设,假设所有的数据页,包括存储真实用户记录和目录项记录的页,都可以存放1000
条记录,那么:
- 如果
B+
树只有 1 层,也就是只有 1 个用于存放用户记录的节点,最多能存放1000
条记录。
- 如果
B+
树有 2 层,最多能存放1000×1000=1000000
条记录。
- 如果
B+
树有 3 层,最多能存放1000×1000×1000=1000000000
条记录。
- 如果
B+
树有 4 层,最多能存放1000×1000×1000×1000=1000000000000
条记录。
你的表里能存放1000000000000
条记录么?所以一般情况下,我们用到的B+
树都不会超过 4 层,那我们通过主键去查找某条记录最多只需要做 4 个页面内的查找,又因为在每个页面内有所谓的Page Directory
(页目录),所以在页面内也可以通过二分法实现快速定位记录。
聚簇索引
上面所说的B+树,我们知道了B+树本身就是一个目录,或者说它本身就是一个索引,它有以下特点:
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
- 页内的记录是按照主键的大小顺序排成一个单向链表;
- 各个存放用户记录的页也是根据页中记录的主键大小顺序排成一个双向链表;
- 各个存放目录项的页也是根据页中记录最小值的主键大小顺序排成一个双向链表;
B+
树的叶子节点存储的是完整的用户记录。
- 所谓完整的用户记录,就是指这个记录中存储了所有列的值。
我们把具有这两种特性的B+
树称为聚簇索引
,所有完整的用户记录都存放在这个聚簇索引
的叶子节点处;
换句话说主键索引就是聚簇索引;
聚簇索引
并不需要我们在MySQL
语句中显式的去创建,InnoDB
存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB
存储引擎中,聚簇索引
就是数据的存储方式(所有的用户记录都存储在了叶子节点
),也就是所谓的索引即数据。
二级索引
上面也说到了聚簇索引是针对主键值时才能发挥作用,那么当索引为其它列的时候,又是怎样的呢?难道只能从头到尾沿着链表依次遍历记录么?
不,我们可以多建几棵B+
树,不同的B+
树中的数据采用不同的排序规则。比方说我们用c2
列的大小作为数据页、页中记录的排序规则,再建一棵B+
树,效果如下图所示:
这个B+树与上边介绍的聚簇索引有几处不同: