InnoDB索引方案
为了使用二分法快速定位具体的目录项,假设所有目录项都可以在物理存储器上连续存储,有以下问题:
- InnoDB使用页为管理存储空间的基本单位,最多只能保证16KB的连续存储空间,记录数据量多可能需要非常大的连续存储空间才能放下所有目录项
- 增删操作,如果把某页的记录都删除,则该页及目录项都没有存在的必要,如果要把后面的目录项都向前移,牵一发而动全身;如果不移动作为冗余放在目录项列表,浪费存储空间
复用存储用户记录的数据页来存储目录项,record_type=1标志目录项记录
目录项记录和普通用户记录的不同点
- 目录项记录record_type为1,普通用户记录的record_type为0
- 目录项记录只有主键值和页的编号两个列,普通用户记录的列由用户自己定义,可能包含多列,还有InnoDB自己添加的隐藏列
- 只有目录项记录的min_rec_flag属性为1,普通用户记录的min_rec_flag属性都是0
如果存储目录项的页也很多,则为这些存储目录项记录的页再生成一个更高级的目录
真正的用户记录都存放在B+树🌲的叶子节点上【第0层】
Page Header部分PAGE_LEVEL属性代表这个数据页作为节点在B+树中的层级
聚簇索引
B+树本身就是一个目录/一个索引,有以下两个特点:
- 使用记录主键值的大小进行记录和页的排序
- 页【包括叶子节点和内节点】内的记录按照主键的大小排序排成一个单向链表,页内的记录被划分成若干个组,每个组中主键值最大的记录在页内的偏移量会被当作槽依次放在页目录项中,可以在页目录中通过二分法快速定位到主键列等于某值的记录
- 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表
- 存放目录项记录的页分为不同的层级,在同一层级中的页也是根据页中目录项记录的主键大小排序排成一个双向链表
- B+树的叶子节点存储的是完整的用户记录【所有列的值,包括隐藏列】
在InnoDB存储引擎中,聚簇索引就是数据的存储方式【索引即数据,数据即索引】
二级索引
聚簇索引只能在搜索条件是主键值时才能发挥作用【B+树中的数据都是按照主键进行排序】
多建几颗B+树,不同的B+树中的数据采用不同的排序规则
与聚簇索引的不同:
- 使用记录c2列的大小进行记录和页的排序
- 页【包括叶子节点和内节点】内的记录是按照c2列的大小顺序排成的一个单向链表,页内的记录被划分成若干个组,每个组中c2列值最大的记录在页内的偏移量会被当作槽一次存放在页目录中
- 各个存放用户记录的页也是根据页中记录的c2列大小顺序排成的一个双向链表
- 存放目录项记录的页分为不同的层级,在同一层级中的页也是根据页中目录项记录的c2列大小顺序排成一个双向链表
- B+树的叶子节点存储的不是完整的用户记录,而只是c2列+主键这两个列的值
- 目录项记录中不再是主键+页号的搭配,变成了c2列+页号的搭配
C2列并没有唯一性约束,也就是说满足搜索条件c2=4的记录可能有多条,只需要在该B+树的叶子节点处定位到第一条满足搜索条件c2=4的记录,然后沿着由记录组成的单向链表一直向后扫描即可**【页内的记录是按照c2列的大小顺序排成的一个单向链表】,各个叶子节点组成了双向链表**,搜索完了本页记录后可以跳到下一页面中第一条记录继续向后扫描
查找c2=4记录的过程:
- 确定第一条符合条件的目录项记录所在的页【定位到页42(2<4<9)】
- 通过第一条符合条件的目录项记录所在的页面确定第一条符合条件的用户记录所在的页【定位到第一条符合c2=4的用户记录所在页为34/35(2<4≤ 4)】
- 在真正存储第一条符合条件的用户记录的页中定位到具体的记录【如果在页34定位到第一条就不需要再到页35定位第一条记录】
- 这个B+树只记录了c2和主键两个列,在定位到第一条符合条件的用户记录后,需要根据该记录中的主键信息到聚簇索引中查找到完整的用户记录,这个通过携带主键信息到聚簇索引中重新定位完整记录的过程也称为回表,然后再返回到这颗B+树叶子节点处沿单向链表继续向后搜索其他满足条件的记录【找到一条就去聚簇索引找到完整记录】
因为需要回表才能获取完整的记录,这种B+树也称为二级索引/辅助索引
联合索引/复合索引/多列索引
可以同时以多个列的大小作为排序规则,即同时为多个列建立索引
- 每条目录项记录都由c2列、c3列、页号这三部分组成,记录先按c2列的值进行排序,c2相同的情况下再按照c3列的值进行排序
- B+树叶子节点处的用户记录由c2列、c3列和主键c1组成
本质上也是一个二级索引
InnoDB中B+树索引的注意事项
- 根页面万年不动窝【页号不再改变】【存簇索引根节点在某个页面,数据字典中的一项信息】
- 每当为某个表创建B+树索引【聚簇索引默认存在】时,就会为这个索引创建一个根节点页面,最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录也没有目录项记录
- 向表中插入用户记录时,先把用户记录存储到这个根节点中
- 在根节点中可用空间用完时继续插入记录,1⃣️将根节点中所有记录复制到一个新分配的页a中,2⃣️对这个新页进行页分裂操作,3⃣️得到另一个新页b,新插入的记录会根据键值【主键值/二级索引对应的索引列值】的大小分配到页a/页b中,根节点升级为存储目录项记录的页,需要把页a和页b对应的目录项记录插入到根节点中【由空叶子节点形成树的过程】
- 内节点中目录项记录的唯一性
目录项记录的内容是索引列加页号的搭配,但对于二级索引来说不够严谨,为了让新插入的记录找到在哪个页,需要保证B+树同一层内节点的目录项记录除页号字段以外是唯一的【二级索引内节点的目录项记录的内容由:索引列的值、主键值、页号构成】
图6-16如果插入列值为1的记录,则该记录会找不到该往页4插还是页5
如图6-17,先把新记录的列值与页3各目录项记录的列值比较,如果列值相同,可以接着比较主键,B+树同一层中不同目录项记录的列值+主键的值肯定不同,最后肯定能定位到唯一一条目录项记录
对二级索引记录来说,先按二级索引列的值进行排序,如果列值相同的情况下,再按主键值排序,为c2列建立索引相当于为(c2,c1)列建立一个联合索引
对于唯一二级索引【当为某列或列组合声明UNIQUE属性时,便会为这个列或列组合建立唯一二级索引】来说,也可能会出现多条记录键值相同的情况【声明为UNIQUE属性的列可能存储多个NULL值;MVCC服务】,唯一二级索引的内节点的目录项记录也会包含记录的主键值
- 一个页面至少容纳2条记录
- InnoDB一个数据页至少可以存放2条记录,如果只能存放一条则目录层级会非常多,最后叶子节点也只有一条记录,效率大打折扣
- 如果B+树叶子节点只存储一条记录,内节点存储多条记录也是可以发挥B+树作用的,但是为了避免B+树层级增长过高,InnoDB要求所有数据页都至少可以容纳两条记录