在使用sql进行join时,我们总是希望连接字段在被驱动表上建立有索引。这样可以加快sql的执行效率,关于join的执行过程以及优化策略,可以查看 “join的执行过程”。

但是,有时候即使我们在被驱动表上的指定字段上建立了索引,join的执行过程仍然没有走索引。接下来,作者介绍几种工作中经常遇到的导致不走索引的场景,看看有没有你遇到的?

编码不一致导致索引失效

为了方便下文描述,我们建立如下表结构:

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL,
  `name` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`),
  key `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_detail` (
  `id` int(11) NOT NULL,
  `name` varchar(32) DEFAULT NULL,
  `other_info` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
   KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表t_user中有两个字段,id和name,其中id是主键索引,name为普通索引。表t_user_detail中有三个字段,id,name和other_info。其中id是主键索引,name字段为普通索引。

然后通过存储过程,向t_user中插入100条数据,向t_user_detail中插入1000条数据。

drop procedure puser_detail;
delimiter ;;
create procedure puser_detail()
begin
	declare i int;
	set i=1;
	while(i<=1000)do
		insert into `t_user_detail` values(i,concat('name',i),concat('user_info',i));
		set i=i+1;
	end while;
end;;
delimiter ;
call puser_detail();
drop procedure puser_;
delimiter ;;
create procedure puser_()
begin
	declare i int;
	set i=101;
	while(i<=200)do
		insert into `t_user` values(i,concat('name',i));
		set i=i+1;
	end while;
end;;
delimiter ;
call puser_();

索引失效

执行以下join查询(为了防止mysql优化,影响结果的验证,这里使用straight_join替换join),并查看其执行计划:

select * from t_user u straight_join t_user_detail d on  d.name = u.name  and u.id = 111 ;

执行计划如下:

mysql left join 用不到索引 left join on不走索引_forkjoin

通过查看查询语句的执行计划,我们发现t_user为驱动表,t_user_detail为被驱动,而且在sql执行的过程中,并没有使用t_user_detail表的name索引,而是对t_user_detail进行了全表扫描。

这里我先说一下原因:表t_user和t_user_detail中的name字段的字符编码类型不同,其中t_user表中name字段的编码类型是utfmb4,t_user_detail表中name字段的编码类型是utf8。我们可以通过以下sql语句进行确认。
查看t_user表中各个字段的编码类型:

SHOW FULL COLUMNS FROM t_user;

mysql left join 用不到索引 left join on不走索引_sql_02

查看t_user_detial表中各个字段的编码类型:

SHOW FULL COLUMNS FROM t_user_detail;

mysql left join 用不到索引 left join on不走索引_mysql_03


这里我们虽然知道了字符编码类型不同会导致索引失效,但是具体原因是什么呢?

为了说明字符编码类型不同,导致查询语句不走索引为题之前,我们先了解下utf8和utf8mb4这两种字符类型编码之间的差异。

utf8和utf8mb4之间的关系

utf8和utf8mb4是mysql中的两种字符集。在mysql中,utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。因此,MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。为了方便utf8字符集类型到utf8mb4转换,在mysql中吧utf8mb4设计成了utf8的超集,所在当需要将utf8字符集转换成utf8mb4字符集时,只需要将指定字段的编码改为utf8mb4就可以了,无需做额外的调整。

不走索引问题分析

因为utf8mb4是utf8的超集,当遇到两种编码类型的数值进行比较时,为了防止数据内容截断丢失,mysql首先会将utf8类型的值,转换成utf8mb4类型后,在进行比较。

基于上面字符类型的转换规则,我们在来分析一下,下面sql的执行流程:

select * from t_user u straight_join t_user_detail d on  d.name = u.name  and u.id = 111 ;

因为t_user的name字段编码类型是utf8mb4,而t_user_detail的name字段编码类型是utf8,所以在sql执行前,mysql会对t_user_detail中的name字符类型转换。上面的sql也就变成了这样:

select * from t_user u straight_join t_user_detail d on  convert(d.name using utf8mb4) = u.name  and u.id = 111 ;

我们知道,索引之所以能够加速查询,主要是因为索引是有序的数据结构,对索引数据结构不熟悉的老铁可以参考 你真的了解索引吗?,而在索引字段上加上了函数后,函数执行的结果是无序。也就无法再利用索引的有序性,加速数据查询了,只能通过全表扫描的方式来完成数据的查询,这就是导致索引失效的根本原因。

为了加深你的理解,你可以看一下,下面这个sql语句的执行流程:

explain 
select * from t_user_detail d straight_join t_user u on u.name = d.name and d.id = 111 ;

查询语句的执行计划如下:

mysql left join 用不到索引 left join on不走索引_forkjoin_04

通过查看sql语句的执行计划,可以发现,在join的过程中,使用了t_user中的name索引。在这里,为什么索引又生效了呢?

因为在这条sql语句中,字符类型转换函数是加在d.name上的,而join过程中使用的索引是u.name,这样并不会破坏索引的有序性。

解决方案

既然我们发现了导致索引失效的原因是:在索引字段上使用函数导致的。那么我们就从问题产生的根本原因出发来解决问题。既然在索引字段上使用类型转换函数会导致索引失效,那么我们在join的过程中,将连接字段的字符类型设置成一样就可以避免类型转换的出现。

1.修改字段的字符编码类型

alter table t_user_detail modify name varchar(32) CHARACTER SET utf8mb4 default null;

这种修改字段类型的字符编码类型的方式可以根本的解决该问题,但是,如果被修改的表的数据量很大,或者线上业务,暂时不允许执行DDL的话,那么可以采用修改sql语句的方式来解决类型转换的问题

2.使用convert函数

可以按照如下方式修改sql语句:

select * from t_user u straight_join t_user_detail d on  d.name = convert(u.name USING utf8) and u.id = 111 ;

可以将连接字段的字符类型修改成和索引索引字段字符类型相同,这样就可以防止在索引字段上执行类型转换函数,导致索引失效的问题。

使用convert函数转变字段编码类型后,查询语句的执行计划如下:

mysql left join 用不到索引 left join on不走索引_mysql_05

上文介绍了"字段编码类型不同"导致查询语句不走索引的场景,其实除了这个场景外,还有其他会导致查询不走索引的情况,限于篇幅的原因,我们放在下一篇文章介绍。