前言
文章基于MySql 5.7.24分析,部分图片源于网络,是MySql索引学习笔记。
MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。
这里只关注BTree索引,这是平时实战中使用最多的索引。
一 MySql索引概要
MySql索引是帮助MySql高效获取数据的排好序的数据结构。索引存储在文件里面。
索引就好比书的目录,你可以通过目录直接找到对应的目标,而不需要从第一页翻起
逐页查找,运气不好,就得从头翻到尾。
简单来说,索引是一种存储在文件里面排好序的数据结构。
MyISAM存储引擎表test01,在MySql的data里面有三个文件:
test01.frm:存储表结构;
test01.MYD:存储数据;
test01.MYI:存储索引;
InnoDB存储引擎表test02,在MySql的data里面有二个文件:
test02.frm:存储表结构;
test02.ibd:存储数据和索引;
二 索引基础数据结构和算法
为什么使用B+Tree作为索引的数据结构,为什么不用二叉树、红黑树、B-Tree等等?
索引提高获取数据效率的本质是什么?
1、磁盘的读写原理
1.1 磁盘立体示意图
一个磁盘由大小相同且同轴的圆形盘片组成,这个旋转轴称为盘片主轴。而所有盘片之间是绝对平行的,
在每个盘片的存储面上都有一个磁头,磁头与盘片之间的距离非常小。所有的磁头连在一个磁头控制器上,
由磁头控制器负责各个磁头的运动。磁头可沿盘片的半径方向动作(实际是斜切向运动),
每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有
多磁头独立技术,可不受此限制)。而盘片以每分钟数千转到上万转的速度在高速旋转,这样磁头就能
对盘片上的指定位置进行数据的读写操作。
1.2 俯视示意图
磁盘示意图和磁盘俯视图。
盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。
磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,
我们下面假设磁盘只有一个盘片和一个磁头。
当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,
即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,
磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间(速度慢,费时),然后磁盘旋转将目标扇区
旋转到磁头下,这个过程耗费的时间叫做旋转时间(速度较快)。
MySql数据存储在磁盘上,一般来说需要经过寻道和旋转,将磁头对应这个数据扇区上方,
才能从磁盘上读取这条数据。如果我们能够通过某种方式明确的告诉计算机去磁盘的哪里存取数据,
而不是每次都让计算机去磁盘上“瞎忙”,地毯式搜索数据,存取数据的效率将会高很多。
而索引,正是MySql用来解决减少磁盘I/O,提高存取效率的手段。所以,提高MySql存储效率
的本质就是想办法减少磁盘I/O次数,尽快从磁盘上获取数据。
1.3 看一个例子,假设用一个树状结构优化查找效率
假设表上有7行两列数据,最左侧为物理地址,如果查找Col2的某一行数据,需要逐个遍历获取对应数据。
比如,查找23这行数据,需要从第一行开始查找,逐行查找,直到找到位置,总的要查找7次。
(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的,有可能查找更多次数,效率更低)。
为了加快Col2查找,可维护最右侧的树型结构,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针。
比如,查找23这行数据,3次就能找到23这个索引键值,通过对应的物理地址指针,直接就能获取23这行数据,
获取数据效率更高。
这就是索引的目标,减少磁盘I/O次数,提高MySql获取数据的效率。
下面讨论MySql为什么选择我们最常打交道的B+Tree实现BTree索引。
2、二叉树
MySql为什么不选择二叉树作为索引数据结构?
如果使用二叉树作为索引数据结构,以Col1建立索引,这里Col1从1到7是顺序的,建立出来数据结构是这样的:
在极端情况下,如果某个列数据顺序增长,建立出来的索引结构也是顺序存储的,
跟从表里面顺序查找的效率是一样的,会失去索引存在的意义,这就是不用二叉树作为索引数据结构的原因。
3、红黑树
MySql为什么不选择红黑树作为索引数据结构?
如果使用红黑树作为索引数据结构,以Col1建立索引,这里Col1从1到7是顺序的,建立出来数据结构是这样的:
从红黑树结构可以看到,整棵树是失衡的,如果查找7的时候,会经历四次查找,
同时,每一个节点只能有两个子节点,如果数据量特别大的时候,树的深度非常大,
整棵树查找效率非常低,也不是MySql索引数据结构的最佳选择。
4、Hash
MySql用Hash作为索引数据结构的优缺点,为什么Hash用得哪么少?
如果我们给Col1每行数据都算一个Hash值,查找的时候直接通过Hash值查找到对应的数据,
这样非常的快,磁盘能直接就定位到数据。但是用Hash有个弊端,如果要进行范围查找,
Hash处理不了范围查找,而实际SQL中,范围查找也是非常常见的。索引Hash在个别值查找
时效率是不错的,比如身份证字段建一个Hash索引,范围查找用得比较少的,效率会比较不错,
千万不要在使用范围查找的列建立Hash索引。所以,在实际中使用Hash索引比较少。
5、B-Tree树
综合上面的数据结构,MySql自然会想到要用一种平衡的结构去解决索引存储问题。
自然会想到B-Tree(不要读成B减Tree,要不然就跟回老家把亲戚辈分叫错了一样尴尬,它是B树,balance)。
在分析B-Tree之前,还需要了解局部性原理与磁盘预读:
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,
因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,
即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是
计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,
预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分
割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数
据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始
位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
假设数据库有两列数据,如下:
如果设定B-Tree的Degree为4,将Col1存储到B-Tree,结构如下:
是不是感觉很清爽,如果要查找7,最多两次就搞定,效率很高。
B-Tree特性:
如下图,是从网上找的一个B-Tree简要结构,并不是磁盘存储的真正结构,是抽象出来的,
用它来说明B-Tree的特性比较直观。
- 度(Degree)-节点的数据存储个数;
- 叶节点具有相同的深度;
- 叶节点的指针为空;
- 节点中的数据key从左到右递增排列;
15,56,77等等为索引大节点中的小节点,每一个小节点均为key-value结构,key存储键值,value存储对应的行数据。
当我们查询数据时,cpu从磁盘把部分索引文件内容加载到内存,在内存查找到目标值,然后有目标的去磁盘拿
我们的目标数据,减少磁盘I/O操作。然而使用B-Tree建立索引文件后,每一个节点上都有数据data,
而每次从磁盘加载到内存的数据是有限制的,一般为一页或几页(一般一页的大小为4k),如果一个节点数据很大的时候,
我们需要很多次磁盘I/O操作才能把一个节点载入到内存,效率很低,就失去了索引的意义。
所以,这就是MySql不使用B-Tree作为索引数据结构。
6、B+Tree树
经过以上分析,终于见到MySql索引数据结构B+Tree,B+Tree是B-Tree的一个变种。B+Tree特性:
- 非叶子节点不存储data,只存储key,可以增大度;
- 叶子节点存储文件指针或数据,但是MySql不同引擎存储的内容不一样;
- 顺序访问指针,提高区间访问的性能;
为什么要使用B+Tree作为索引的数据结构?
一般使用磁盘I/O次数评价索引结构的优劣。
B+Tree在非叶子节点上不存储数据,只存索引的键值,这样能保证每一个节点上数据非常小,
MySql根据B+Tree的特性和局部性原理,将B+Tree节点的大小设为等于一个页,每次新建节点直接申请
一个页的空间,这样就保证一个节点物理上也存储在一个页里,就实现了一个节点的载入只需一次I/O,
大大减少磁盘I/O次数,提高获取数据的效率。同时B+Tree的度d一般会超过100,因此h非常小(一般为3到5之间),
只需要几次I/O操作就能把索引检索完,效率非常高。
三 MyISAM引擎索引实现(非聚集)
MyISAM存储引擎上的索引是非聚集索引,因为索引文件和数据文件是分离存储的。
MyISAM存储引擎和InnoDB存储引擎在叶子节点上存储的数据不一样。
MyISAM存储引擎表test01,在MySql的data里面有三个文件:
test01.frm:存储表结构;
test01.MYD:存储数据;
test01.MYI:存储索引;
1、MyISAM主键索引
MyISAM主键索引的数据结构如下:
比如以主键Col1建立索引,在叶子节点上存储的是文件指针,如果我们要查找30这行数据,
则先在索引文件(MYI)根据B+Tree查找到30这条数据的文件指针,然后通过文件指针
直接从磁盘数据文件(MYD)中查找到对应的数据。如果是范围查找,因为索引已经排好序了,
直接从磁盘读取对应范围的数据。
2、MyISAM非主键索引
MyISAM存储引擎的非主键索引存储结构与其主键索引的存储结构相同,查找方式一样。
四 InnoDB引擎索引实现(聚集)
InnoDB是聚集索引,因为索引文件和数据文件聚集在一起,合并在一起了。
InnoDB存储引擎表test02,在MySql的data里面有二个文件:
test02.frm:存储表结构;
test02.ibd:存储数据和索引;
数据文件本身就是索引文件,表数据文件本身就是按B+Tree组织的一个索引结构文件。
1、InnoDB主键索引
InnoDB引擎主键索引在叶子节点存储的不是文件指针,与MyISAM存储引擎在叶子节点存储文件指针不同,
因为索引文件和数据文件是合在一起的,所以,在叶子节点存储的是对应索引键值的数据。
我们只要通过索引遍历就能查找到数据,相比MyISAM引擎,能够减少一次通过文件指针
从数据文件(ibd)获取数据的一次磁盘I/O。
为什么InnoDB表必须有主键,并且推荐使用整型的自增主键?
MySql InnoDB引擎会基于主键建立B+Tree数据结构,如果没有主键,
它会选择唯一数据列(比如唯一索引列)建立B+Tree数据结构。
如果主键和唯一键都没有,它就随便给你搞一列建立B+Tree数据结构。
所以,MySql推荐使用整型的自增主键,因为整型比较大小查找比字符串快,
整型一比较就知道大小,字符串需要根据ASCII码进行比较,相对较慢,
同时,在插入数据的时候,使用字符串移动次数比较频繁,并且使用字符串也比较占用存储空间,
所以从空间和时间上推荐使用整型自增主键。
2、InnoDB非主键索引
InnoDB非主键索引在叶子节点存储的是该行数据的主键值。
通过索引结构找到对应数据的主键,然后通过主键物理地址去磁盘ibd文件
直接获取数据。
为什么非主键索引结构叶子节点存储的是主键值?
1)一致性,为了主键索引和非主键索引数据一致性。如果非主键索引上面存储的是数据,
哪么如果数据发生变动,需要修改主键索引上的数据,同时也需要维护非主键索引的数据,
从事务的角度来说,这样就需要采取措施保证主键索引和非主键索引上的数据要一致,
大大增加了处理数据的复杂性,可能会产生数据不同不,哪么使用不同索引查找到的数据就不一致了。
所以在非主键索引上存储主键值,如果数据发生改动,只需要修改主键索引上的数据,
而非主键索引存储的主键值不需要变动。本质上就是保证主键索引和非主键索引都是
从同一个地方拿数据,保证数据的一致性。
2)节省存储空间
非主键索引叶子节点存储主键的另外一个原因就是为了节省存储空间,能把数据归类在一个地方
供大家调用,何必要把数据到处散落呢。
五 联合索引实现原理
单值索引只是联合索引字段为1个的情况,联合索引由多个字段构成。
比如通过表里面三个字段建立联合索引。
联合索引查找分析:
1)如果第一个字段是101,直接就能比较出大小,无需第二个字段比较。
2)如果第一个字段101一样,则通过第二个字段比较大小。
3)如果第一个103和第二个html5一样,则通过第三个字段比较大小,
如此依次类推,就是联合索引查找方式。
六 最左前缀实现原理
比如有通过三个字段seqNo,name, date构建联合索引,如下:
最左前缀就是多列字段构建的联合索引,最左边的字段优先比较。
最左前缀原理有很多使用原则,比如:全值匹配法则,最左前缀法则等等。
七 总结
1、MySql BTree索引使用B+Tree数据结构;
2、MySql不同存储引擎在B+Tree叶子节点存储的数据不一样,
MyISAM主键索引和非主键索引存储文件指针,InnoDB主键索引存储数据,InnoDB非主键索引存储主键值;
3、联合索引由多个字段联合构建,最左前缀原理查找时最左边的字段优先比较。
索引优化博大精深,这篇主要分析BTree底层数据结构,下篇结合索引数据结构去分析、使用和优化。