数据页组成一个双向链表,每个数据页中的记录按主键值从小到大的顺序组成一个单项链表。为每个数据页中的记录生成一个页目录,根据主键查找某条记录时,在页目录中通过二分法快速定位对应的槽,再遍历槽对应组中的记录就可以快速找到指定的记录。
简单的索引方案
- 下一个数据页中用户的主键值必须大于上一个页中用户记录的主键值
假设一个页最多存储3记录,页号是10,存储的主键ID分别是2,4,6,新增第4条记录时,主键值是5,发现页号是10的记录主键最大值是6,也就是说要将第4条记录插入到页号为10里面,必然会将主键为5的记录移动到新的页。这就是页分裂的过程。
页分裂:对页面进行增删改的过程中,为了保证下一个数据页中用户的主键值必须大于上一个页中用户记录的主键值的这种状态,必须将一些记录进行移动。
- 为所有的页建立目录项:每个页对应一个目录项
其中目录项包括:页的用户记录中最小的主键值key + 页号page_no
所有的目录项在物理存储器上连续存储的缺点:
- 记录非常多时,目录项需要非常大的连续存储空间,而页只有16KB大小
- 对记录进行增删改操作时,需要批量对目录项进行移动;或是容易存放在目录项列表中,造成存储空间浪费
InnoDB中的索引方案
- 叶子节点:存放
用户记录的数据页 - 非叶子节点(内节点):存放
目录项记录的数据页
索引分类
- 聚集索引
- B+树的节点都使用记录主键值进行排序
1)页内的记录按【主键值】的顺序排成单向链表。(包括叶子节点和内节点)
2)存放用户记录的页根据【主键值】的顺序排成双向链表
3)存放目录项记录的页分为不同层级,同一层级根据页中目录项记录的【主键值】排成双向链表
搜索条件是主键值时才发挥作用
- 叶子节点存储完整的用户记录
- 二级索引
- 使用记录的索引列进行排序
1)页内的记录按【索引列】顺序排成单向链表。(包括叶子节点和内节点)
2)存放用户记录的页根据【索引列】顺序排成双向链表
3)存放目录项记录的页分为不同层级,同一层级根据页中目录项记录的【索引列】排成双向链表
注意:无论是用户记录的页还是目录项记录的页,页内的记录都会划分成若干组
- 叶子节点存储索引列+主键
- 目录项记录的页存储索引列+主键值+页号
- 回表:只发生在二级索引上,携带主键ID到聚集索引中重新定位完整记录的过程
回表的代价:二级索引执行回表的操作记录越多,查询性能越低,
所以在某些查询选择全表扫描效率反而更高。
二级索引记录对应的聚集索所在的页面是无序的,
每次执行回表操作都相当于读取一个聚集索引页面,这些随机I/O带来的性能开销大。
- 联合索引:多个列构成的索引,也只会建立一个B+树
每个索引都会建立一个B+树
注意事项
根页面万年不动
一个B+树索引的根节点自创建后页号不再改变,新增记录都在根节点的基础上进行插入以及页分裂的操作。
内节点中的目录项记录的唯一性
目录项记录的页存储索引列+主键值+页号,主键值就可以区分目录项记录的唯一性。
一个页至少容纳2条记录
避免B+树层级增长过高
思考
- InnoDB是如何区分一条记录是普通的用户记录还是目录项记录的?
每条记录都会有record_type的属性(0:普通的用户记录 1:目录项记录 2:Infimum记录 3:Supremum记录) - 目录项记录和普通用户记录有什么区别?
- 目录项记录的record_type值是1,而普通用户记录的record_type值是0
- 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列
- 记录头信息的min_rec_mask的属性,只有在存储目录项记录的页中的主键值最小的目录项记录的min_rec_mask值为1,其他别的记录的min_rec_mask值都是0
- 通过主键值在存储用户记录的页定位过程
- 为什么要为每个表创建字段ID?
- 什么情况下会导致页分裂?
InnoDB规定下一个页的用户记录主键值必须大于上一个页用户记录主键值。当一个页不能再容纳新的记录时,会分配新的一个页存储该记录,那么此时也就是页分裂的过程 - 为什么要进行回表,直接都把完整的记录存放在叶子节点上不好吗?
浪费存储空间;要维护与聚集索引的用户完整记录,两份数据都要维护,那么就引入了强一致性的事务,性能下降; - 为什么目录项在物理存储器上不连续存储?
InnoDB使用页作为存储的基本单位,也就是16KB。但随着记录越来越多,那么需要更大的存储空间才能把所有目录项放下,这样不太现实; 对记录进行删除时,目录项也没必要存在,要把其他目录项移动。或者冗余保留目录项,但浪费存储空间。