在工作中,如果我们想要提高一条语句的查询速度,通常都会想对字段建立索引。
但是索引不是万能的。建立了索引,并不意味着任何查询语句都能走索引扫描。
稍不注意,可能查询语句就会导致索引失效,从而走了全表扫描,虽然查询的结果没有问题,但是查询的性能大大降低。
索引存储结构长什么样?
索引的存储结构跟MySQL使用哪种存储引擎有关,因为存储引擎就是负责将数据持久化在磁盘中,而不同的存储引擎采用的所有数据结构也会不相同。
MySQL 默认的存储引擎是 InnoDB ,它采用B+Tree 作为索引的数据结构
在创建表时,InnoDB 存储引擎默认会创建一个主键索引,也就是聚簇索引,其他索引都属于二级索引。
MySQL 的 MyISAM 存储引擎支持多种索引数据结构,比如 B+Tree 索引、R树索引、Full-Text索引。
MyISAM 存储引擎在创建表的时候,创建的主键索引默认使用的是 B+Tree索引。
虽然,InnoDB 和 MyISAM 都支持 B+Tree 索引,但是它们数据的存储结构实现方式不同。不同之处在于:
- InnoDB 存储引擎:B+Tree 索引的叶子节点保存数据本身
- MyISAM 存储引擎:B+Tree 索引的叶子节点保存数据的物理地址
举个例子,展示两种存储引擎的索引存储结构的区别。
这里有一张 t_user 表,其中 id字段为主键索引,其他的都是普通字段。
如果使用的是MyISAM 存储引擎,B+Tree 索引的叶子节点保存数据的物理地址,即用户数据的指针,如下图:
如果使用的是 InnoDB 存储引擎,B+Tree索引的叶子节点保存数据的本身,如下图:(图中叶子节点是单链表,实际上是双链表)
InnoDB 存储引擎根据索引的类型不同,分为聚簇索引和二级索引。它们的区别在于,聚簇索引的叶子节点存放的是实际数据,所有完整的用户数据都存放在聚簇索引的叶子节点上,而二级索引的叶子节点存放的是主键值,而不是实际数据。
如果将 name 字段设置为普通索引,那么这个二级索引如下图所示:(图中为单链表,实际上是双链表)叶子节点仅存放主键值。
接下来会举例说明查询过程会怎么选择用哪个索引类型:
在我们使用 [主键索引] 字段作为条件查询的时候,如果要查询的数据都在 [聚簇索引] 的叶子节点里,那么就会在 [聚簇索引] 中的 B+Tree 检索到对应的叶子节点,然后直接读取要查询的数据。如下面这条语句:
select * from t_user where id = 1;
在我们使用 [二级索引] 字段作为条件查询的时候,如果要查询的数据都在 [聚簇索引] 的叶子节点里,那么需要检索两颗 B+Tree :
- 先在 [二级索引] 的 B+Tree 找到对应的叶子节点,获取主键值
- 然后用上一步获取的主键值,在 [聚簇索引] 中的 B+Tree 检索到对应的叶子节点,然后获取要查询的数据。
上面这个过程叫做 回表 , 如下面这条语句:
// name 字段为二级索引
select * from t_user where name="林某";
在我们使用 [二级索引] 字段作为条件查询的时候,如果要查询的数据在 [二级索引] 的叶子节点,那么只需要在 [二级索引] 的 B+Tree 找到对应的叶子节点,然后读取要查询的数据,这个过程叫做 覆盖索引,如下面这条查询语句:(覆盖索引省去了回表操作)
// name 字段为二级索引
//由于 id 会存在 二级索引的叶子节点,所以不需要进行回表操作->覆盖索引
select id from t_user where name="林某";
上面这些查询语句的条件都用到了索引列,所以在查询过程都用上了索引。
但是并不意味着,查询条件用上了索引列,就查询过程就一定都用上了索引,接下来会例举一些会导致索引失效的情况,而发生全表扫描。
对索引使用左或左右模糊匹配
当我们使用 左或左右模糊匹配的时候,也就是 like %xx 或 like %xx% 的=这两种方式都会造成索引失效
比如下面的 like 语句,查询 name 后缀为 [林] 的用户,执行计划中的 type = ALL 就代表了全表扫描,而没有走索引。
// name 字段为二级索引
select * from t_user where name like '%林';
如果查询的 name 前缀为 林 的用户,那么就会走索引扫描,执行计划中的 type=range 表示走索引扫描 ,key = index_name 表示 实际走了 index_name 索引。
// name 字段为二级索引
select * from t_user where name like '林%';
为什么 like 关键字 左或者 左右 模糊匹配无法走索引呢?
因为索引B+Tree 树是按照 [索引值]有序排列的,只能根据前缀进行比较。
eg:下面这张二级索引表,是以 name 字段有序排列顺序的:
假设我们要查询 name 字段前缀为 [林] 的数据,也就是 name like '林%',扫描索引的过程:
- 首节点查询比较:林这个字的拼音大小比首节点的第一个索引值中的陈字大,但是比首节点的第二个索引值 [周] 字小,所以会选择去 节点2 继续查询;
- 节点 2 查询比较:节点 2 的第一个索引值中的陈字的拼音大小比林字小,所以继续看下一个索引值,发现节点 2 有与 林 字前缀匹配的索引值,于是就往叶子节点查询,即叶子节点 4 ;
- 节点 4 查询比较:节点 4 的第一个索引值的前缀符合 林 字,于是就读取该行的数据,接着继续往右匹配,直到匹配不到前缀为 林 的索引值。
如果使用 name like '%林' 方式来查询,因为查询的结果可能是 [ 陈林,周林...]等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
对索引使用函数
有时候我们会使用一些 MySQL自带的函数来得到我们想要查询的结果,这时,如果查询条件中的索引字段使用函数,就会导致索引失效。
比如下面这条语句的查询条件中对 name 字段使用了 LENGTH 函数,执行计划中的 type = ALL,代表了全表扫描:
// name 为二级索引
select * from t_user where length(name)=6;
不过从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值就是函数计算后的值,所以就可以通过扫描索引来查询数据。
eg:对 length(name) 的计算结果建立一个名为 idx_name_length 的索引
alter table t_user add key idx_name_length ((length(name)))
然后再用下面这条查询语句,这时候就走索引了:
对索引进行表达式计算
在查询条件中对索引进行表达式计算,也是无法走索引的。
比如,下面这条查询语句,执行计划中 type = ALL,说明是通过全表扫描的方式查询数据的:
explain select * from t_user where id + 1 = 10;
但是,如果把查询语句的条件改成 where id = 10 - 1 ,这样就不是在索引字段进行表达式的计算了,于是就可以走索引查询了。
对索引隐式类型转换
如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,在执行计划中会发现这条语句会走全表扫描。
eg:原本的 t_user 表增加了 phone 字段,是 二级索引且类型是 varchar。
然后再条件查询中,用整型作为输入参数,此时执行计划中的 type = ALL,所以是通过全表扫描来查询数据的。
select * from t_user where phone = 1300000001;
但是如果索引字段是整型类型,查询条件中的输入参数即使是字符串,是不会导致索引失效,还是可以走索引扫描的。
explain select * from t_user where id = '1';
为什么第一个例子会导致索引失效,而第二个不会呢?
要明白这个原因,就需要了解MySQL是会将字符串转换成数字处理,还是将数字转换成字符串处理。
- 如果规则是 MySQL 会自动将 [字符串] 转换为 [数字],就相当于 select 10 > 9,这个就是数字比较,所以结果应该是 1;
- 如果规则是 MySQL 会自动将 [数字] 转换为 [字符串] ,就相当于 select "10" > "9",这个是字符串比较,字符串比较大小是逐位从高到低逐个比较(按照ascii码),所以会先拿 '1' 和 '9' 比较,结果为 0.
上面的结果为 1 ,表示 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转换为数字,然后再进行比较。
前面的例子一中,会走全表扫描:
//例子一的查询语句
select * from t_user where phone = 1300000001;
这是因为 phone 字段为字符串,所以 MySQL 会自动把字符串转换为数字,所以这条语句相当于:
select * from t_user where CAST(phone AS signed int) = 1300000001;
可以看到:CAST 函数作用在 phone 字段,而 phone 字段是索引,也就是对索引使用了函数,而对索引使用函数会导致索引失效!
例子 2 中,会走索引扫描:
//例子二的查询语句
select * from t_user where id = "1";
这是因为字符串部分是输入参数,而id索引为整型,也就是需要将字符串转换为数字,所以这条语句相当于:
select * from t_user where id = CAST("1" AS signed int);
可以看到,索引字段并没有用任何函数,CAST 函数是用在了输入参数,因此是可以走索引扫描的。
联合索引非最左匹配
对主键字段建立的索引叫做聚簇索引,对普通字段建立的索引叫做二级索引。
那么多个普通字段组合在一起创建的索引就叫做联合索引,也叫组合索引。
创建联合索引的时候,我们需要注意创建时的顺序问题,因为 联合索引 (a,b,c) 和 (c,b,a) 在使用的时候会存在差别。
联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
比如,如果创建一个(a,b,c)联合索引,如果查询条件是以下几种,就可以匹配上联合索引:
- where a = 1
- where a = 1 and b = 2 and c = 3
- where a = 1 and b = 2
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
- where b = 2
- where c = 3
- where b = 2 and c = 3
有一个比较特殊的查询条件:where a = 1 and c = 3:
这种严格意义上属于索引截断,不同版本有不同的处理方式
MySQL5.5,前面的 a 会走索引,在联合索引找到主键值后,开始回表,到主键索引读取数据行,Server 层从存储引擎获取到数据行后,然后在 Server 层再比对 c 字段的值。
MySQL5.6之后,有一个索引下推功能,可以在存储引擎层进行索引遍历的过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,再返还给Server 层,从而减少回表次数。
索引下推:截断的字段不会在 Server 层进行条件判断,而是会被下推到 [存储引擎层] 进行条件判断(因为 C 字段的值是在 (a,b,c) 联合索引里的),然后过滤出符合条件的数据后再返回给 Server 层,由于在引擎层就过滤掉大量的数据,无需再回表读取数据来进行判断,减少了回表次数,从而提升了性能。
为什么联合索引不遵循最左匹配原则就会失效?
原因是,在联合索引下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。
也就是说,如果想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
WHERE 子句中的 OR
在 where 子句中,如果 or 前的列是索引列,而在 or 后的条件列不是索引列,那么索引会失效。
举个例子,比如下面的查询语句,id是主键, age 是普通列。从执行计划的结果看,是走全表扫描。
select * from t_user where id = 1 or age = 18;
这是因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。
解决方法:将 age 字段设置为索引即可。
可以看到 type = index merge ,index merge 的意思就是对 id 和 age 分别进行扫描,然后将这两个结果集进行合并,这样做的好处是避免了全表扫描。
总结
- 当使用 左 或者 左右 模糊匹配的时候,也就是 like %xx 或 like %xx% 这两种方式都会造成索引失效;
- 当我们在查询条件中对索引列使用函数,就会导致索引失效。
- 当我们在查询条件中对索引列进行表达式计算,也无法走索引
- MySQL在遇到字符串和数字比较的时候,会将字符串转换为数字,然后进行比较。如果字符串是索引列,而条件查询时数字,那么索引列会发生优势类型转换,由于隐式类型转换是通过 CAST 函数实现的,相当于对所有列使用了函数,所以就会导致索引失效。
- 联合索引正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效
- 在 WHERE 子句中,如果 OR 前的条件列是索引列,而在OR后的条件列不是索引列,那么索引会失效。