阅读该部分内容时,需要提前了解B+Tree树基本知识点,否则可能有些内容你并不能很好的体会到。对于下面几点内容如果不是很清楚,可以阅读我之前写的Mysql简叙一文中的内容进行了解。
- 每个索引都是一颗B+Tree,最下面的一层是叶子节点,其余都是内节点。而叶子节点中存储的都是用户相关数据,而内节点存储的是页节点相关信息。
- InnoDB会自动为主键(没有会自动添加)建立聚族索引,聚族索引包含了所有的用户数据。
- 二级索引中的用户数据是通过索引列和主键组成的,利用二级索引找到对应的记录时,如果需要获取整条记录信息需要通过二级索引中的主键通过回表的方式从聚族索引中获取。
- B+Tree中每层节点都是按照索引列从小到大的顺序排列的。
- 查找数据时是从B+Tree的根节点开始往下找。
索引使用示例
对于后面的示例,我们定义如下表结构:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用户姓名',
`age` int(11) NOT NULL COMMENT '年龄',
`phone_no` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '手机号',
PRIMARY KEY (`id`),
KEY `index_user_age` (`user_name`,`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
从表结构中我们可以知道,在这个表中我们存在两个索引,一个是根据主键和用户数据创建的聚族索引,另外一个则是user_name和age组成的二级索引,所以在这个表中存在两颗B+Tree树。
全值匹配
SELECT * FROM t_user WHERE user_name = 'tom' AND age = 18;
我们建立的index_user_age索引两个字段都在最后的查询条件中,这个查询的执行过程如下:
- 首先通过index_user_age这个B+Tree树能很快的找到对应的记录。
- 因为需要返回所有数据,而index_user_age该索引树中只有user_name,age,id信息,所以需要通过回表的方式从聚族索引找到对应的整条用户数据然后返回。
可能你一直有一个疑问就是我们的查询语句中条件的顺序是user_namge和age,这个顺序和索引中字段的顺序一样能用到索引。但是如果我的查询语句中条件的顺序是age和user_name时,还能用到索引吗?答案是能用到索引。在Mysql中存在查询优化引擎,它会根据你创建的索引里的字段顺序去优化你的SQL,所以对于这一点完全不用担心。
从上面的分析结果可以看出它们的结果是一样的,所以你是不用担心查询条件中的字段顺序的。
最左匹配原则
SELECT * FROM t_user WHERE age =18;
上面的SQL能用到索引吗?如果你了解B+Tree索引知识,你就能知道是不行的。因为它不符合最左匹配原则,那为什么会这样子呢?我们先看看index_user_age索引具体是什么样子的。
通过上图我想你很清楚索引的结构了。最后一层就是我们的索引数据,它们的顺序是按照user_name->age排列的,我们在使用index_user_age索引时,我们先只能先按照user_name按顺序找到所有的对应的索引数据,然后再根据age去做进一步的筛选,所以对于上面的SQL语句我们无法用到index_user_age索引。那下面这个语句可以用到index_user_age索引吗?
SELECT * FROM t_user WHERE user_name = 'tom';
相信你心中已经有了答案。我们直接看这个语句的分析结果,看看是不是和我们说的一致。
从explain结果可以看出,第一条SQL是无法用到任何索引的,但是第二条可以用到index_user_age索引。
索引下推
我们修改index_user_age索引,在原先index_user_age的基础上增加phone_no索引字段,创建新的索引index_user_age_phone索引。修改后的表结构如下:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用户姓名',
`age` int(11) NOT NULL COMMENT '年龄',
`phone_no` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '手机号',
`address` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_user_age_phone` (`user_name`,`age`,`phone_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
现在我有一个查询如下:
SELECT * FROM t_user WHERE user_name = 'jack' AND phone_no = '11111111111';
我现在想问的是这个查询中能用到索引吗?phone_no这个能用到索引吗?根据前面的最左匹配原则可以知道这个查询能用到索引index_user_age_phone,好像phone_no这个是没法用到索引的。对于index_user_age_phone索引来说,它内部索引列的字段顺序是按照user_name->age->phone_no按顺序排列的,对于我们的查询条件,搜索时我们只能用到user_name这个列了。但是实际上phone_no这个列也能用到,只不过不是用在搜索查询中。
通过二级索引查询记录时,我们只能获取到索引列上的值和主键,如果我们的返回结果中需要返回该记录的所有字段信息,我们就不得不通过回表的方式从聚族索引再次获取该记录的完整信息。这个过程如下所示:
首先我们根据user_name找到所有满足记录数据,然后通过回表的方式从对应的主键索引(聚族索引)中找到完整的用户数据,然后再筛选phone_no这个条件。回表这个操作你可以理解为通过主键ID从聚族索引中获取用户数据,索引回表是有消耗的,虽然通过主键查找记录很快,但是通过user_name匹配到的数据很多时,还是会影响查询速度的。那有没有方式可以减少回表的次数呢?
估计聪明的你肯定想到了,从图中我们可以看到现在有三条记录满足 user_name=jack 这个条件,正常情况下我们需要回表三次。如果我在回表前就筛选phone_no这个查询条件,这样是不是就能减少我们的回表次数呢?如果在回表前判断phone_no这个条件,这时我们只有一条记录满足条件,回表次数也从3次变成1次了。
对于上面这种方式,我们就叫做 索引下推 。这个特性是在Mysql5.6及以后才版本才提供,对于之前的Mysql版本是没有这个优化的。
列前缀匹配
在开发中我们可能会听别人说like查询无法使用索引,但是实际真的如此吗?其实这个跟我们之前说的最左匹配原则很相似,例如有下面的这个查询:
SELECT * FROM t_user WHERE user_name LIKE 't%';
这个查询的意思是查找名字以t开头的用户,对于这个查询是能使用到索引的。在这个查询中会用到index_user_age_phone这个索引,因为在二级索引树中,索引的内容是按照user_name、age、phone_no这个顺序排列的。而我们的查询是查user_name以字母t开头的,所以在这个查询中我们的索引能生效,这个方式我们通常称作列前缀匹配。但是下面这个SQL索引是不生效的。
SELECT * FROM t_user WHERE user_name LIKE '%t%';
这个SQL的意思是查找名字中包含字母t的用户,所以这个时候索引不会生效。我们可以通过explain结果来验证我们的结论。
从上面的explain结果我们可以看出,对于like 't%' 即前缀匹配是可以用到索引的,而后面的 like '%t%' 是无法用到索引的。
索引用于排序
索引不仅仅只是用作查询条件,同时索引还能作用于排序。例如现在有下面这个排序:
SELECT * FROM t_user ORDER BY user_name,age,phone_no LIMIT 10;
例如上面这个SQL,它是可以用到索引的。因为对于index_user_age_phone这个索引树,它里面的数据是按照user_name、age和phone_no组合的值从小到大排列的,所以这里是能用到索引的。可能你会问,默认情况下排序是ASC,跟索引里面一样是从小到大,但如果换成DESC,即从大到小的顺序排列那还能使用索引吗?答案是可以,只要字段顺序没变,不管是ASC还是DESC它都能用到索引。(注意:如果使用explain查看结果可能发现它没有使用任何索引,这可能是因为你表中的数据量太少,Mysql扫全表比使用索引更快,因为使用二级索引需要回表)。
那是不是排序都能用上索引呢?答案是不是的。并不是所有的排序都能用上索引,对于下面的几种情况是无法通过索引来排序的。
- ASC和DESC混用
SELECT * FROM t_user ORDER BY user_name ASC ,age DESC ,phone_no ASC LIMIT 10;
- where子句中出现非排序的索引列
SELECT * FROM t_user WHERE address = 'xxxx' ORDER BY user_name LIMIT 10;
- 排序列包含非统一个索引的列
SELECT * FROM t_user ORDER BY user_name,address;
- 排序列使用了函数
SELECT * FROM t_user ORDER BY UPPER(user_name)
索引用于分组
索引用于分组这个跟排序类似,基本上的原则也都一样。
回表和索引覆盖
前面说过,有时候使用explain分析语句时发现并不是按照一些规则来的,至于为什么会如此这个就跟回表有关了。我们在使用二级索引找到对应数据后,如果我们要返回的列不在索引中,这个时候我们就需要进行回表。回表它需要通过ID再去聚族索引中找到原始数据,并且这里面并不是一个顺序I/O。如果对于大量的数据需要回表时,Mysql往往有时候不会使用二级索引而是直接扫表。因为大量的回表它的效率有可能还没有扫全表的效率高。
对于我们查询而言,如果应该尽量不使用号返回所有字段,而应该只取我们需要的字段返回即可。同时我们还可以通过索引覆盖*的方式来减少回表。例如我有一个查询是通过user_name查询用户的年龄和手机号,我在创建索引时不仅仅只在user_name列上创建索引,我可以创建一个由user_name、age和phone_no三个字段组成的索引,而查询的返回值只需要返回这三个字段即可。
建索引的一些建议
对于索引而言,并不是越多越好。索引是可以提高我们的查询效率,但是如果索引过多也会带来一些负面影响。每个索引在InnoDB中都是一颗B+Tree,多一个索引就多一颗B+Tree,这会占用过多的磁盘空间。同时在更新和删除时,需要同时更新和删除索引中的数据,这样会导致数据库的写入性能受影响。很多情况下面,有些索引是多余的,我们应该精简我们的索引。通常我们在建索引时应该有下面的几个原则:
- 只为用户搜索、排序或分组的列上建立索引。
对于这点我想很好理解,如果有些列我们根本用不上那就没必要建索引,多建的索引并不能给我们带来好处反而还浪费了服务器的资源。 - 为基数大的列建立索引。
什么是基数呢?例如性别这个列,正常情况下我们一般也就是存三个值。例如:男、女、未知。即使我们的表里面存了100万条数据,它的值还是在这三个里面,而这个列的基数就是3。为了加快搜索我们在性别这个列上面创建索引,这种方式并不能带来查询性能上的明显改善,对于这种列我们就没有必要在这个列上单独创建索引了。 - 索引列的类型尽量小。
例如对于整数类型在Mysql中就有好几种,例如tinyint,mediumint,int,bigint。它们所占的空间依次递增,而能表示的数字大小也同样依次递增。如果一个整数列的值能用int表示完整我们就不应该用bigint,数据类型越小,索引所占的空间也就越小,那么在一个业内能存放的数据也就越多。这样就能减少磁盘I/O的次数,减少磁盘I/O的次数也就意味着性能的提升。 - 删除冗余和重复索引
什么样的索引算是冗余和重复索引呢?例如我创建了一个以user_name列和age列组合而成的索引index_user_age,然后又创建了一个以user_name列的索引index_user,这种情况下索引就重复了。对于这种情况,我们可以删掉index_user索引,因为index_user_age索引完全可以替代index_user索引的效果,所以我们没必要多加一个索引浪费服务器资源。