一、索引的实现结构
1. 哈希表:产生哈希冲突并且无法用于范围查询
2. 二叉搜索树:在极端情况下可能会退化成链表,性能不稳定;在大数据量的情况下,树会很高,增加磁盘的IO次数
3. 平衡二叉树:为了维护树的平衡,需要不断旋转,降低性能;在大数据量的情况下,树会很高,增加磁盘的IO次数
4. 红黑树:旋转和分裂操作会降低性能;不适合大数据量的查询,当数据量很大时树会很高,这会增加磁盘的IO次数
- 节点不是黑色就是红色
- 根节点和叶子节点为黑色
- 如果某个节点为红色,那么它的两个子节点应该为黑色
- 从任意节点出发到其子孙叶结点的所有简单路径应该包含相同数目的黑色节点
5. m阶B树:不能很好的解决范围查询问题;由于节点不仅存储索引值还会存放数据,这会降低单个节点存储的索引值数量,增加磁盘的IO次数
- 所有叶子结点位于同一层
- 根结点分支数的下限为2,上限为m
- 中间结点分支数的区间为[m/2,m]
- 关键字个数 = 分支数 - 1(非叶节点)
- 各个结点的关键字按从小到大的递增顺序排列
- 每个结点的结构为(n,P0,K1,P1,K2,…,Kn,Pn)
- 假设某个结点的关键字编号为K1-Kn,指针编号为P0-Pn,P0指向结点的关键字区间为(-oo,K1],Pn所指向的结点关键字区间为(Kn,+oo),Pi指向结点的关键字区间为(Ki,Ki+1]
6. m阶B+树:只有叶节点才会存放数据,增加了单个节点存储的索引值数量,减少磁盘的IO次数;并且叶节点包含了全部的索引值,更好的解决了范围查找的问题
- 有K个分支的非叶结点含有K个元素,每个元素不存储数据只用来做索引
- 非叶结点中只包含对应子树的最大或最小元素以及指向这些子树的指针
- 叶子结点包含了全部元素的信息以及指向相应记录的指针,并且叶子结点会按照关键字大小由小到大顺序连接
总之,无论是二叉搜索树,还是 AVL 树,亦或是红黑树,它们都是二叉树的一种,特点都是每个结点最多只有两个子结点,如果存储大量数据的话,那么树的高度会非常高,这会增加磁盘的IO次数。因此在 存储大量数据的背景下,二叉树是不适合当做索引的数据结构。
二、索引分类
索引的本质是一种数据结构,也就是组织数据的一种方式(底层实现的数据结构是B+树)! 与B树相比,B+树将所有数据都存储在叶子节点,因此一个节点就能够存储更多的索引,一个三层高的B+树就能够存储上千万条数据
1. 聚簇索引: 也称作一级索引,索引的叶节点保存了完整的数据记录,InnoDB存储引擎下的主键索引就是聚簇索引。它将索引元素和数据元素存储在同一棵B+树中。
frm:存储表的结构
ibd:存储索引和数据的联合文件
叶子节点存储的可能是索引值对应的单条数据记录也可以是当前索引值所对应记录附近的多条记录(如下方第五、六章所示)
2. 非聚簇索引: MyISAM存储引擎下的索引是非聚集索引,它的数据文件和索引文件是分开存放的。
frm:存储表的结构
MYD:存储表的数据行
MYI:存储表的索引
三、页的概念
3.1 页的结构
在操作系统中页(4KB)是一个逻辑单位,遵循局部性原理,可以减少IO次数,同样在InnoDB中也存在页的概念(16KB),它是存储数据的基本单位,其结构图如下所示:
3.2 向页中插入数据
3.3 记录的头信息
- delete_flag
- 删除过程是怎么样的?
1>将delete_flag置为1
2>所有被删除的记录会组成一个垃圾链表,垃圾链表的空间为可重用空间
- 为什么要这样做而不是直接删除?
每一次删除数据时如果从磁盘删除,会消耗性能,这是不必要的,采用标志方法会减少性能消耗 - n_owned:页中每个组中的非最大值记录的该字段均为0,只有每个组中最大记录的该字段才有值->该组的记录数量
- heap_no:记录的编号,页中最小记录(infimum)和最大记录(supremum)的编号分别是0和1,所以用户插入记录的编号由2开始
- record_type:最小记录(infimum)和最大记录(supremum)的类型分别是2和3,用户插入记录的类型是0,非叶结点目录项的类型是1
- min_rec_flag:只有目录项记录的min_rec_flag字段才可能为1,而普通用户记录的min_rec_flag字段都是 0。
3.4 Page Director
其中页目录中存储的是每组中最大数据记录的地址偏移量也称为槽(slot)。假设要查找a=4的数据,首先将存储a=4数据的这一页从磁盘加载到内存中,之后再取页目录中的数据,判断a=4在页目录中哪个槽所对应的分组中 (二分查找) ,找到该分组后从上到下逐条遍历每条数据。(通过本组的槽可以定位到本组的最大记录,通过上一组的槽可以定位到本组的最小记录)
分组的规则:
- Infimum 分组只能存储1条记录
- Supremum 分组只能存储1-8条记录
- 剩下的分组只能存储4-8条记录
分组的步骤:
- 最初,新的页只有Infimum和Supremum两组.
- 之后,开始插入记录时,记录会被插入到Supremum组,并且n_owned加1.
- 当Supremum组已经有了8条记录。如果再插入一条记录的话,原来的组会被分为两个组,一个组4条记录,一个组5条记录,修改对应的记录的n_owned值并往Page Directory里新增一个Slot,指向新增组中的最大记录
3.5 页满后的插入操作
注: 如果主键需要手工插入则会涉及到记录迁移,这会消耗性能,如果主键是自增的话,则不会涉及到记录迁移,减少性能消耗
3.6 在没有索引的时候如何查找记录
情况1:数据存放在一个page上
step1:通过二分查找快速定位数据所在组对应的slot
step2:遍历组里的记录,找到对应的数据
情况2:数据存放在多个page上
由于Page Director只存在页上,并不在更高层次上,所以我们没有办法快速查找到数据所在的Page。索引就是层次更高的"Page Directory"(功能类似,快速查找)
四、创建表
首先创建含有四个字段(a,b,c,d)的数据表tb1,并向其中插入一些数据。
CREATE TABLE tb1(
a INT PRIMARY KEY,
b INT,
c INT,
d INT
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO tb1 VALUES(1,2,4,5);
INSERT INTO tb1 VALUES(2,4,7,6);
INSERT INTO tb1 VALUES(4,8,6,3);
INSERT INTO tb1 VALUES(6,2,3,2);
INSERT INTO tb1 VALUES(3,4,9,8);
五、主键索引的生成
以下内容都默认处在InnoDB数据引擎下!由于在创建tb1表时就指定了主键字段,所以在向表中插入数据时主键索引就会依据主键字段组织数据,另外只有在主键索引中才会保存全部数据!索引的底层数据结构是B+树,所以只有叶子节点才会存储数据,存储数据的单位是页(16KB),下面我们假设每‘页’只能存储两行数据。主键索引的B+树结构如下:
六、联合索引的生成
创建关于b、c、d字段的联合索引:
CREATE INDEX index_bcd ON TB1(b,c,d);
重点: 随着数据的更新,索引需要不断地进行维护,而且索引自身就需要占用存储空间,所以不可能在每一个索引中都存储一份完整数据。因此只能在主键索引(一级索引)中才会存储完整的数据,在联合索引的叶子节点中只会存储索引字段对应的索引值和该索引值对应的行记录的主键值,之后进行"回表"查询(通过得到的主键值利用主键索引查询到对应行记录的完整数据),如果查询字段被索引覆盖则不需要回表查询!索引进行查询的时候,如果能够尽量避免查询非索引字段最好,这样就不会进行回表查询,加快查询时间。
七、Buffer Pool
内存与磁盘是以页为单位进交互的,假如第一次查询id=1的数据,会加载包含该数据的页,查询完成之后,会将该页放入缓冲池(属于内存范畴),这样下次继续查询的话速度就会很快,直接从内存中读取。
八、free链表
如何判断缓冲池是否有空闲缓冲页?: 首先查看free链表的基结点的count是否为0,如果不为0,说明缓冲池中有空闲的缓冲页,之后查找到free链表中的一个控制块,找到它所对应的空闲缓冲页存储新加载进来的页,之后再将该控制块从free链表中移除。
九、flush链表
当需要更新的数据位于缓冲池中的某一页上,先对缓冲页进行更新(这时候的页叫脏页->内存中的页与磁盘中的页不一致),然后该缓冲页所对应的控制块会被加入flush链表,有一个专门的线程会将flush链表中的控制块所对应的缓冲页刷到磁盘上。
十、LRU链表
缓冲池的大小是有限的,所以有些时候要删除掉一些不常用的页。当磁盘中的某页第一次加载进缓冲池,它只能被加载进LRU链表的old区域。
十一、chunk和Buffer Pool实例
InnoDB不能为每一个Buffer Pool一次性申请特别大的连续空间,所以以chunk为单位申请空间(连续的空间)
十二、补充
1.虽然MySQL索引的底层结构使用的是B+树,但是并不是原封不动地采用,会进行一些改进。比如非叶结点(页)之间也会存在双向指针(上面有一些图没有标明),可以参考页结构。
2.当遍历到索引的某个结点查找索引值进行比较时,采用的是二分查找算法。将数据页加载进内存后,同样会使用二分查找算法在页目录中查找索引字段数据所在组对应的槽(slot),再对组进行自上而下的遍历,最终查找到数据。
觉得本片文章帮助到自己的小伙伴麻烦点点关注!