索引是怎么构建的?

138 阅读12分钟

《MySQL是怎么运行的》读书笔记
前置知识:行格式,数据页结构

MySQL是以数据页(索引页)来作为磁盘和内存交互的基本单位,页的大小是16KB,也就是一次至少从磁盘上读取16KB的数据到内存中。

数据在页中是怎么存储的呢?

页分为7个部分,分别包括文件头,页头,infomum+supermum,用户记录,空闲空间,页目录,文件尾。文件头里存着当前页的页号,上一页的页号,下一页的页号等信息,页号可以唯一定位一个页的位置。因此,页和页之间形成了双向链表

用户记录里储存着用户向表中插入的所有记录,排列顺序不是按插入顺序的,是按照主键的大小从小到大的顺序连接成单向链表,因为每条记录都是以compact行格式在用户记录中存储的,行格式中有一部分叫记录头,记录头里有一个属性记录着下一条数据的偏移量,另外infomum+supermum是两条虚拟记录,分别代表着最大记录和最小记录,也就是说infomum的下一条就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条就是supermum。另外,页的上一页中所有记录也都小于当前页的记录,页的下一页中的所有记录也都大于当前页。

在一个页中如何查找数据——分组

假如我们要在某个页中查找一条数据该怎么查找呢?

最笨的方法是从infomu开始,沿着链表一直往后找,总有一天会找到,或者找不到。

但是如果数据量很大的时候就比较慢了,所以innodb的作者是什么人,他们怎么会用这么慢的方法呢,于是就从书的目录中找到灵感,把页中的记录进行分组。将所有记录(包括infimum+supermum)划分为几个组,将每个组的最后一条记录(也就是组内最大的那条记录)的地址偏移量提取出来按顺序放到页目录,这些地址的偏移量被称为槽,所以页目录是由槽组成的。分组的规则:对于最小记录infomum所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 18 条之间,剩下的分组中记录的条数范围只能在是 48 条之间。这时就可以用二分法进行快速查找了。先找到中间的槽的位置,看它所对应的的主键值比我要查找的目标主键值大还是小,然后不断收缩边界,直到找到目标所在的槽,去槽对应的分组中遍历查找就可以了,由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。

在多个页中如何查找数据——目录项记录页

刚才只是说如何在一个页中查找某条记录,但是表中数据量大的情况下不可能所有的记录都存放在一个页中呀!如果在很多个页中查找记录的话,就要分两步:

  1. 先定位到记录所在的页
  2. 再在页中用二分法查找相应的记录

所以现在的问题就是如何定位到相应的页?

在没有索引的情况下,我们只能从第一个页沿着双向链表一直往下找,在每个页中使用二分法去查找指定的记录,因为要遍历所有的页,所以这种操作必然很耗时。所以我们要给这些页维护一个目录

我直接说innodb的索引方案

作者使复用了数据页来存储目录项记录,什么是目录项记录?用户自己插入的记录叫做普通用户记录,里面有我们记录的所有列,也包括隐藏列。而目录项记录只有两个列,一个是主键列,一个是页号列。主键列存储该页中最小的主键,页号列存储该页的页号。既然都是数据页,那怎么区分存储的是用户记录还是目录项记录呢?在记录头中有个记录类型的属性:0-普通用户记录、1-目录项记录、2-最小记录、3-最大记录,用此来区分。

这样当我们在一批较大的用户记录页中去查找某个记录时就可以先在他们的目录项记录页中通过二分法找到对应的页号,再去这个页中用二分法快速定位目标记录。

虽然说目录项记录只存储主键列和页号列,相比用户记录所需要的空间要小很多,但是无论怎样一个页只有16KB,能存放的目录项记录也是有限的,如果表中的数据量太多,以至于一个页不足以存放所有的目录项记录该怎么办呢?

当然是再多整几个目录项记录页喽!如果目录项记录页也很多,那么就可以在目录项记录页之上再加一层目录项记录页目录项记录页

那么现在如果我们想根据主键值查找一条用户记录大致步骤:

  1. 从根页面通过二分法开始找下一层的目录项记录页,再通过二分法找再下一层的目录项记录页,直到找到用户记录页为止
  2. 用户记录页中通过二分法找到具体的目标记录

这些由目录项记录页用户记录页构成的整体结构就是B+树,从图中可以看出来,我们的实际用户记录其实都存放在B+树的叶子节点上。这个B+树本身就是一个索引,B+ 树的叶子节点存储的是完整的用户记录,这样的索引成为**聚簇索引(聚集索引)**,这个聚簇索引并不需要我们自己显示的创建,而是innodb自动为我们创建,另外,聚簇索引就是存储数据的方式,所有的用户记录都存储在聚簇索引的叶子节点上,这就是所谓的**索引即数据,数据即索引**。并没有其他专门的地方再存储一次全部数据。

一棵B+树能存储多少数据?

假设,假设,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录,所有

存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么:

如果B+ 树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放100 条记录。

如果B+ 树有2层,最多能存放1000×100=100000 条记录。

如果B+ 树有3层,最多能存放1000×1000×100=100000000 条记录。

如果B+ 树有4层,最多能存放1000×1000×1000×100=100000000000 条记录。哇咔咔~这么多的记录!!!

你的表里能存放100000000000 条记录么?所以一般情况下,我们用到的B+ 树都不会超过4层,那我们通过主键

值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内

有所谓的Page Directory (页目录),所以在页面内也可以通过二分法实现快速定位记录。

二级索引

上边介绍的聚簇索引只能在搜索条件是主键值时才能发挥作用,因为B+ 树中的数据都是按照主键进行排序的。那如果我们想以别的列作为搜索条件该咋办呢?

那我们就用同样的方式以指定的列来比较大小构建索引,只不过叶子节点不存储完整的用户记录,只记录该列和主键,这样的索引成为二级索引(辅助索引,非聚簇索引)

通过二级索引只能拿到要查找记录的的主键值,我们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录。这个过程就是回表

为什么我们还需要一次回表操作呢?直接把完整的用户记录放到叶子节点不就好了么?你说的对,如果把完整的用户记录放到叶子节点是可以不用回表,但是太占地方了呀~相当于每建立一棵B+ 树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。

联合索引怎么办

我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+ 树按照c2

和c3 列的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2 列进行排序。
  • 在记录的c2 列相同的情况下,采用c3 列进行排序

如图所示,我们需要注意一下几点:

  1. 每条目录项记录都由c2 、c3 、页号这三个部分组成,各条记录先按照c2 列的值进行排序,如果记录的c2 列相同,则按照c3 列的值进行排序。
  2. B+ 树叶子节点处的用户记录由c2 、c3 和主键c1 列组成。

千万要注意一点,以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:

  • 建立联合索引只会建立如上图一样的1棵B+ 树。
  • 为c2和c3列分别建立索引会分别以c2 和c3 列的大小为排序规则建立2棵B+ 树。

B+树

m叉树就是度是m的二叉排序树,相比二叉树,它可以降低树的深度,但是它缺少规则约束,每个节点虽然最多可以存放m-1个key,但是我可以只放一个key就创建下一个节点,这样就成了斜树,造成了空间浪费。于是给m叉树加上一些规则约束,要求每个节点必须存放至少一半的key才能创建新的节点,根节点除外,这样一颗M叉树就成了B树。

而B树的每个节点都存放的有key和value,如果非叶子节点不存放value,只存放key,全部数据都在叶子存放,且形成双向链表,那么就成为了B+树

索引为什么不用B树?

  1. B+树的所有用户记录都存放在叶子节点,形成了双向链表,使得范围查询非常高效

  2. B+树的非叶子节点不存储完整的数据,只存储key,使得每个非节点可以存放更多的数据,树的深度更小,从而减少磁盘的IO的次数

  3. 查找操作可能在B树的非叶子节点就结束了,B+树的查找操作总是在叶子节点结束,这可能查找速度略慢,但性能更稳定

索引为什么不用其他数据结构

1. 为什么不用哈希表做索引?

  1. 虽然哈希表等值查询比较快,但哈希函数设计的再好也会产生哈希冲突,哈希冲突导致数据散列的不均匀,数据量大的时候,会产生大量线性查询,效率低。
  2. 等值查询可以,但是遇到范围查询,只能挨个遍历,hash就不合适了

2. 为什么不用二叉树做索引?

二叉树是无序的,需要挨个遍历

3. 那二叉排序树呢?

既然二叉树无序,那二叉排序树呢?左子树上的所有节点都小于根节点,右子树上的的所有节点都大于根节点,查找变快了,但是如果插入的顺序是按照从小到大的顺序,那树的形态就会变成斜树,这样查询的效率又退回到线性查询了

4. 那平衡二叉树呢?

平衡二叉树不会出现斜树的情况,插入数据的时候会保持平衡,因为它的平衡因子保证了左右子树的高度差绝对值不大于1,那它是不是就合适了呢?也不是,因为插入数据的时候需要改变树的形态来保证树的平衡,相当于用插入的成本来弥补查询的效率,一旦遇到插入操作特别多查询特别少的情况,就要花费大量的开销进行旋转来保持平衡,所以也不合适

5. 红黑树呢?

红黑树相比于平衡二叉树最大的特点就是最长子树不超过最短子树的二倍就行,也就是说它在插入元素的时候不需要进行频繁的旋转操作,保证了查询效率的同时,插入效率也高,但还不够完美,数据量大的时候,树的深度会更深。树的深度越深,磁盘IO读写次数越多,所以要想尽可能减少磁盘IO次数,还要考虑树的深度问题