前言
回顾一下日常的研发工作,是不是我们经常会给数据表建立索引呢?即使我们中的大多数并不是专业的DBA,但仿佛从直觉上来讲建立和使用索引就会提升MySQL数据表的查询效率。那这到底是为什么呢?
带着这个问题我们进一步思考一下性能瓶颈究竟在哪里,可能大多数人已经脱口而出了,MySQL数据表的查询性能瓶颈就往往在磁盘IO上,今天我就从磁盘IO的角度和你聊聊MySQL的索引,和你一起探讨一下为什么使用索引后数据表的查询性能会有所提高,使用索引时我们又需要注意些什么。
MySQL为什么慢
首先不同于Redis 和Memcache 这类的内存数据库,MySQL数据库中数据表的数据记录是以存储引擎规定的特定数据格式存储在物理磁盘上的。
我们知道在整个计算机的存储体系中有寄存器有高速缓存Cache 有内存当然也有磁盘。在这些存储介质中越往上的存储介质数据的存取数据越快但存储的数据越少,越往下的存储介质数据的存取数据越慢但可存出的数据越多。
MySQL数据表的数据记录就持久化在整个计算机存出体系里存取数据最慢的磁盘中所以在MySQL中执行一条SQL查询与据最后实的阶段就是磁盘的IO阶段。
如果在MySQL的查询语句执行过程中如果我们能有效减少最耗时的磁盘IO阶段的时间,那么整个查询数据的执行效率会将会被大大提升。
MySQL中的BTree
假设有一张数据表表中的数据记录个数为N,在不使用索引的情况下查询数据表中的一条记录理论上的时间复杂都是O(n),因为绝大多数情况下需要对整个表进行顺序遍历才能找到想要的记录。这里可以思考一下如果使用了索引时间复杂度会不会有质的提升呢?带着这个问题我们先从数据结构的角度来看看所应是如何实现的。
B+树的结构
MySQL中有多种索引最主要的是BTree索引,BTree索引对应的数据结构就是B+树;为了看看这颗索引树长成什么样;首先执行这样的SQL语句来定义一张数据表。
less
CREATE TABLE `t_student`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY idx_age (`age`)
) ENGINE = InnoDB;
其中t_student是个学生表,在表中id表示学号,name表示姓名,age指表示年龄;另外表中有两个索引一个是主建索引对应的表字段是id另一个是二级索引index_age对应的表字段是age。
再来看数据表中存储的内容为了便于简单展示索引数的结构,我们假设表中中的数据记录只有7条分别表示7个学生。与之对应的age字段上建立的索引B+树结构如下图所示。
它是一个平衡有序的N叉树它的节点分为叶子节点的和非叶子节点,相对于普通的二叉树来说B+树的非叶子节点可以有多个分支,相对于B树来说B+树的非叶子节点本身是不存储实际数据的。它仅仅用于B+树的检索而最底层的叶子节点用于存储真正的数据。
让我们来看看这个索引树的优势,首先非叶子节点因为不存储实际的数据所以能够有空间保存更多的检索分叉,这有利于降低整个索引树的高度从而減少检索过程中的磁盘IO次数;另外可以看到叶子节点之间是相互连接的,这样在叶子节点层会形成一个有序的双向链表,方便在叶子节点层直接进行数据的橫向遍历进而有利于范围查询。
B+树如何查询数据
接下来再具体看一个查询的例子,假设基于刚才的的学生信息表要查询年龄为十岁的学生。
csharp
select * from t_student where age=10;
来看一下这条SQL语句的执行,这条查询语句会使用字段age上的的BTree索引,查询过程是先从B+树的根节点开始检索,然后按照B+树的检索方式一层一层往下,直到能够找到叶子节点中age=10的索引值;如果这个索引值是存在的,根据索引值对应的数据就可以到数据表中找到对应的整条数据记录了。
那么整个查询过程的时间复杂度是多少呢?由于B+树的查询时间复杂度是O(logN),而根据B+树检索结果来定位数据记录的时间小于O(logN),因此整个查询过程的时间复杂度仍然是O(logN)。
刚才说过 SQL查询过程中如果不使用索引时间复杂度是O(N),而使用了索引以后我们得知时间复杂度降为了O(logN),因此在数据表中数据记录较多的情况下使用索引会大大降低磁盘IO的次数,从而提高SQL语句查询的效率。
优化建议
那是不是只要使用了索引就万事大吉了呢?答案显然是否定的,有了索引仍然可以在減少磁盘IO上做一些文章。接下来我将从減少磁盘IO的角度给出几条的索引方面优化的建议。
InnoDB存储引擎中,主键尽量避免使用很长的字段
每个叶子节点中除了记录了索引值也记录了这个索引值对应的数据记录的指向,前面例子中用id等于某个主建值来表示数据记录的指向;事实上根据MySQL存储引擎的不同,数据记录的指向存储内容也是不同的,这里可以区分一下InnoDB存储引擎和MyISAM存储。
MyISAM存储引擎的索引文件和数据文件是分开的,某个数据表的数据记录是单独存放在数据文件中的,这时索引树的叶子节点中存放的是数据表记录的物理地址。
而在InnoDB存储引擎中某个数据表的数据记录就保存在主建的BTree索引树的叶子节点中,所以可以看到在InnoDB存储引擎中,普通索引的叶子节点的数据存放的是主建。如果主建越长,二级索引的叶子节点只能存储相对来说越少的主键。这会使整个二级索引树相对来说更大,搜索起来可能会需要更多的磁盘IO次数。
在保障索引区分度的情况下,被索引的字段尽量不要太长
在MySQL的InnoDB存储引擎中,BTree索引的每个节点都是一个磁盘页面,又被称为page。可以通过这样的命令来查看,page的大小一般默认为16K。
在BTree索引的检索过程中,每读取一个节点就会进行一次磁盘IO,因此B+树的树高就是通过BTree索引进行检索的磁盘IO次数。因而当被索引字段较短时,一次磁盘IO就可以获得更多的索引键;这时整个B+树的树高会降低从而减少了磁盘IO的次数。
但是也不能盲目地缩短索引字段的长度,我们还需要考虑索引的区分度,索引的区分度又被称为索引的选择性;具体是指不重复的索引值又称为基数与数据表中的全部记录数的比值。
这个比值的取值范围是在(0,1]之间,这个值越靠近于1表示索引列的重复记录数越少,索引的使用就越具有价值。以刚才我们使用的t_student数据表为例看一下索引区分度的计算
假设以name字段来建立索引,则索引区分度可以通过这个SQL语句来查看,语句中主要是比较了不重复的name和总记录数的比值。我们看计算结果为1,说明在这个例子中以name字段作为索引索引区分度非常高。
接下来假设使用了更短的索引键,例如使用name字段最左边的第一个字符来建立索引,通过计算name字段最左边一个字符不可重复的个数与总记录数的比值,可以得到它的索引区分度。
在实际应用中我们可以通过逐步缩短索引字段长度计算索引区分度的方式,来找到既能保持好的索引区分度又相对来说短一些的索引。
查询中可以利用索引覆盖,从而避免不必要的回表
首先说说什么是回表,通俗来讲,通过二级索引无法查到整条的数据记录需要我们根据二级索引中查到的主键,再去主键索引中去查找这个过程就被称为回表。而如果查询所需的信息,恰巧在二级索引中能够得到则可以省去回表的过程,从而减少回表所需要的磁盘IO次数。
假设我们要查找年龄等于10岁的所有学生的id,如果使用下面的查询语句则会发生回表现象。
csharp
select * from t student where age = 10;
因为在二级索引树中没有name的信息,所以需要去主键索引树中去获取
但如果我们使用下面的查询语句就不会发生回表操作。
csharp
select id,age from t_student where age = 10;
因为要查找的是id和age这样就不需要再去主键索引树中查找整条记录了,磁盘1/0次数也就减少了从而提高了查询效率,我们通常又管这种不需要回表的现象称为索引覆盖。
在实际项目实践中,可以适当地使用联合索引来进行索引覆盖避免发生回表;如果t student数据表中建立了(age, name)的联合索引idx_age_name (age,name);那么当需要查找年龄等于10岁的学生的id及姓名时,也就是执行select id, name, age from t_student where age = 10;
语句时就不需要进行回表了。
不要建立太多的索引
索引并不是万能的,建立索引也是有代价的,数据表中任何一条数据的写操作都可能会影响到索引树;随着索引数目的增加表的更新操作会浪费更多的磁盘IO,而且如果在索引区分度非常低的字段上建立索引,对于查询效率不但没有多少提升反而会降低插入、删除、更新等数据表操作的效率。
总结
今天我从磁盘IO的角度和你一起讨论了MySQL的索引,我们知道SQL查询最耗时的部分是在磁盘IO;索引之所以能提高SQL查询的效率也是因为使用了索引能够减少磁盘IO的次数。
MySQL中最常见的BTree索引是基于B+树来实现的,它的查询时间复杂度是O(logN);而完全不使用索引进行全表扫描时查询时间复杂度是O(N)
最后从磁盘IO的角度我给出一些索引优化的建议,首先从通过減少索引树进而減少磁盘O次数的角度,我给出了两点建议:第一点是InnoDB存储引擎中在中主建尽量避免使用太长的字段,让索引树的的叶子节点尽量可以保存更多的记录。第二点在保障索引区分度的情况下我们可以尝试缩短被索引的字段,这样可以让非叶子节点保存更多的索引键;接着从利用索引覆盖避免不必要的回表的角度,我建议可以适当的使用联合索引。最后我想说的是索引并不是银弹,太多的索引也是负担,建议不要建立太多的索引。