1.概述
索引的目的是加快数据的访问,那么如何设计索引才能更快的访问数据呢?
在数据的访问中会涉及到这么几个概念的词语:
1. 索引的数据结构
2.读取数据产生的磁盘IO
在与磁盘交互的时候,IO其实是最大的瓶颈,我们要尽量减少IO的次数,降低一次IO的数据量.
2. 数据交互
索引就像我们平时看的书,如果想要查看具体的内容,我们会在书的目录里面找到对应的页码,然后在具体页码里面,再去寻找具体的某一个部分的数据,就像下图一样:
通过这个值我们就可以很快的查询到我们想要的数据.
但是仔细一想,为什么MySql不采用这个数据结构呢,而是使用B+tree的数据结构,如果业务数据量非常大的时候,我们需要维护很大的索引文件,也是非常占用资源的.
具体的数据是存放在磁盘中,我们通过索引的方式快速的查询到我们需要的数据,那么索引的数据是存放在磁盘还是内存中呢?
其实索引数据同时存在于内存和磁盘中,当需要数据的时候,就会从磁盘的索引文件中读取一定量的索引数据,然后加载到内存中.
索引数据也是需要持久化的,如果mysql服务器重启或者宕机,内存中的所有数据全部丢失了,数据的索引创建是非常消耗性能的,所以不可能每次都是重新创建索引的.
那么问题了?如果索引数据非常大,难道我们一次性把所有的索引数据全部加载内存中吗?可想而知,肯定不是的,那么它一次加载多少数据呢?
答案就是按照块的概念进行读取索引数据,每次只读取N块的数据加载到内存当中.
在与mysql交互的过程中,有一个概念词语叫做:磁盘预读,也就是说并不是我们需要什么数据就只查询对应的数据,而是以页的概念,每次读取N页的数据,如果读取过后的数据已经加载到内存当中了,那么下次有对应的查询时,就会直接从内存中读取对应索引数据,无需从磁盘中读取了.
默认情况下,一页的数据大小是4K,这个其实是和操作系统有关系的,有的可能是8K,也就是说每次数据交换的时候,保证是4K的整数倍即可,这个页其实也就是我们上面所说的块的概念.
在mysql的配置参数当中,通过mysql的命令我们可以查询到设置的每次读取的数据大小,正常mysql设置的是16K,当然了我们也可以根据需要设置对应的值,不过一般情况下,16k大小的数据足够了.
3. 数据结构
从上面的描述当中我们可以看到,索引的结构其实是一种K-V的数据结构,在我们的知识范围内,大概有以下几种:
1. 哈希表
2.二叉树(Binary tree)
3.BST树(二叉搜索树)
4.AVL树(二叉查找树)
5.红黑树
6.B树
7.B+树
3.1 哈希表
大致如图所示:
0...12... 这个就是我们所说的key,右边的就是对应的value.
缺点:
1. 如果数据非常多,哈希表会占用非常大的内存空间,并且数据也不能进行分块读取(哈希表底层还是有数组组成,数据分配的时候,是预先分配内存,并且是连续的存储空间).
2. 哈希存储结构不适合进行范围查找,如果范围查找的话,就需要遍历所有,过滤符合条件的数据,这一定程度时间复杂度就提高了,如果是等值的,效率就非常高了.
They are used only for equality comparisons that use the
=
or<=>
operators (but are very fast). They are not used for comparison operators such as<
that find a range of values. Systems that rely on this type of single-value lookup are known as “key-value stores”; to use MySQL for such applications, use hash indexes wherever possible.
3. 哈希表无论散列算法多优秀,总会有哈希值冲突的情况,如上图所示,一定程度上还是有散列不均匀的情况,严重浪费内存空间.
总结: 哈希数据结构是不适合于像Innodb的,但是适用于Memory存储引擎.
3.2 二叉树
在上面列举的树当中,AVL和红黑树是有序的平衡二叉树,BSL是有序二叉树,但是不是平衡二叉树.
二叉树每个节点就只能有左右两个子节点,而且每个节点只能存储一个key值,随着数据量的增加,索引的文件大小也会随着增加,那么二叉树的深度必定会增加,树的深度增加了,那么我们要获取到想要的数据,IO的访问次数就一定会上升,并且索引文件也会占用非常大的空间.
在与磁盘交互的过程中,就是要减少IO的访问次数和IO数据量,显然二叉树是不适合做索引的存储结构的.
在树的结构中,我们既要保持它的有序,也要保证它的平衡,既然二叉树不能满足我们的需要,那么如果是多叉树就能弥补二叉树只有2个节点的不足.
3.3 B-tree
B-tree其实也是从二叉树演变过来的,它的名字叫做平衡多路查找树.
简单定义:
首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。
一棵m阶的B-Tree有如下特性:
1. 每个节点最多有m个孩子。
2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
3. 若根节点不是叶子节点,则至少有2个孩子
4. 所有叶子节点都在同一层,且不包含其它关键字信息
5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)为关键字,且关键字升序排序。
8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
从网站https://www.cs.usfca.edu/~galles/visualization/Algorithms.html我们可以在线模拟一下b-trees的过程,如下图所示:
这是一个简单的3阶b-trees,也就是每个节点做多只能存放2个key.
下面这个是b-tree的数据结构模型图:
p(n)是指针,指向其他的磁盘块.
16/34.. 是具体的key值
data是对应key的value值.
在我们正常使用的mysql中,默认一次读取时16k的索引数据,假如现在一个磁盘块是16K.
现在我们想获取到key=2的数据:
1.首先获取磁盘块1(16k的数据)
2.判断发现2<16,然后查找磁盘块2(16k的数据)
3.判断发现2<7,然后根据p1指针的地址,获取到磁盘块5
4.最后获取到key=2的具体数据
在这个过程中,我们总共进行3次的IO操作,总共加载了16*3=48k的数据.
我们试着计算一下,如果一个key值对应的占用大小是1k,那么一个磁盘块最多存放16条数据,那么在3次数据的获取中,我们最多只能加载16*16*16=4096条数据.
对于生产环境来说,表中包含十万,百万,甚至千万的数据来说,3层树结构只能获取到4096条数据,简直是太少了,势必会增加树的深度,这样就增加了IO的交互次数,这个不是我们想要的结果.
从上面的模型图上来看,我们可以看的出来,它的问题点在哪里呢?
因为每个key下面都带着data数据,key和指针其实占用的内存空间非常小,最主要的就是因为了磁盘块中包含了data的数据,所以才导致每个固定大小的磁盘块可存储的数据太少了.
这个情况下,b-tree就不是那么合适了,它的缺点就在这里,如果说数据结构中,树的节点上不带有data数据,那么同样空间大小的磁盘块必定会存放更多的数据.B+tree就是我们想要的结果.
3.4 B+tree
简单介绍:
B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
B+Tree相对于B-Tree有几点不同:
- 非叶子节点只存储键值信息。
- 所有叶子节点之间都有一个链指针。
- 数据记录都存放在叶子节点中。
同样我们模拟一下b+tree的结构图:
再来看下它的结构模型图:
p(n)是指针,指向其他的磁盘块.
28/39.. 是具体的key值
data是对应key的value值.
这里和b-tree有个明显的差异,B+tree的data数据只存在于叶子节点,非叶子节点之存放key值,并且所有的data都是按照顺序存放在一层上面,每个叶子节点都有其他叶子节点一个指针指向.
从上图我们可以看出,除了叶子节点存储了data数据以外,非叶子结点是不存储具体的行数据的,只保存key值和指针,这样对于同样大小的磁盘就大大增加了存储的索引记录数.
同样定义一个磁盘块是16K,每个key占用10byte的大小,那么每个磁盘块就可以存储16*1024/10~=1600条,那么同样读取48k的数据情况下,我们就可以查询到1600*1600*16~=4KW的数据,相当于b-tree树来说,完全不是一个数量级上的.
叶子节点上的数据是按照索引有序存储的,我们检索数据就有2种方式进行检索:
1.直接从叶子节点遍历查询
比如说全表扫描的sql语句,就不需要再从根节点一次次的往下查询了,直接遍历叶子节点.
2.从根节点随机查找
我们知道在mysql不同的数据类型,占用的空间大小不一样,所以在设计索引的时候,索引key的大小越小越好,这样每个磁盘块可以存储的记录数就会很多,这样我们在交互数据的时候,就可以加载到更多的数据了.
在一般的表设计时,主键我们一般都是采用自增的模式,那么为什么要采用自增呢?
从叶子节点来看,这些都是有序的排列的,如果我们不是采用的自增的索引,那么在有新的数据插入的时候,就会涉及到页分裂的问题,并且还会影响到非叶子节点,如果采用的是自增的形式,那么只需要在叶子节点后面追加即可,无需修改中间的数据结构.