在日常开发中,我们经常会发现,Mysql中一些逻辑上一样的sql,往往性能差异很大,至于为什么会发生这样的问题,今天我们就看看几个常见的案例
案例一:条件字段函数操作
假设我们有一张表如下建表语句
代码语言:javascript复制mysql> CREATE TABLE `tradelog` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`operator` int(11) DEFAULT NULL,
`t_modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`),
KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
现在我们的需求就是获取年份中7月份的数据,逻辑上并不复杂,我们也回想很快想到下面语句
代码语言:javascript复制select count(*) from tradelog where month(t_modified)=7
但是我们发现在数据量大的时候,这条语句返回的时间非常长,但是我们的确建立了t_modified的索引,为什么还是这么慢呢
此时我们可以看看t_modified的索引树,如下图
上面的数字是函数计算的值,此时我们发现,如果我们使用下面语句会快速的根据上图的绿色箭头找到对应的值
代码语言:javascript复制select * from tradelog where t_modified='2018-7-1'
实际上,B 树提供了快速定位的能力,来源于同一层节点的有序性。
但是,如果计算month()函数的话,你会看到传入7的时候,在树的第一层就不知道该怎么办了,也就是说,对索引做函数操作,会破坏索引值的有序性,因此优化器就决定放弃走树的搜索。
但事实上,优化器并没有放弃使用索引,但是优化器可以遍历索引,可以选择主键索引和t_modified字段索引,优化器发现t_modified索引树比较小,最终还是选择了字段索引,
可以看到extra的值是Using index,说明使用了覆盖索引。虽然使用了索引,但是也是全索引扫描。
此时我们可以使用下面语句,实现我们需要的结果,下面语句也用上了索引的快速定位能力,但是比较繁琐
代码语言:javascript复制mysql> select count(*) from tradelog where
-> (t_modified >= '2016-7-1' and t_modified<'2016-8-1') or
-> (t_modified >= '2017-7-1' and t_modified<'2017-8-1') or
-> (t_modified >= '2018-7-1' and t_modified<'2018-8-1');
总结:发现如果我们对字段使用函数,会破坏索引的有序性,是无法使用索引快速定位的功能,而只能全索引扫描,需要注意的是,函数没有破坏索引的有序性,优化器也不会考虑使用索引的快速定位能力的,
案例二:隐式类型转换
我们先看看下面语句
代码语言:javascript复制mysql> select * from tradelog where tradeid=110717;
我们发现上面虽然我们的tradeid有索引,但是他还是会走全表扫描,这个又是为什么呢,
其实我们发现tradeid他的类型是varchar(32),而输入的参数值确实int,索引要做类型转换。在mysql中,字符串和数字做比较的话,是将字符串转换成数字.
上面的语句实际上在优化器里面就如下面语句
代码语言:javascript复制mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;
从上一个案例我们知道使用了函数是不会走索引的,优化器会放弃索引的快速定位能力。
案例三:隐式字符编码转换
我们建立下面表,且插入一些数据
代码语言:javascript复制mysql> CREATE TABLE `trade_detail` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`trade_step` int(11) DEFAULT NULL, /*操作步骤*/
`step_info` varchar(32) DEFAULT NULL, /*步骤信息*/
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into tradelog values(1, 'aaaaaaaa', 1000, now());
insert into tradelog values(2, 'aaaaaaab', 1000, now());
insert into tradelog values(3, 'aaaaaaac', 1000, now());
insert into trade_detail values(1, 'aaaaaaaa', 1, 'add');
insert into trade_detail values(2, 'aaaaaaaa', 2, 'update');
insert into trade_detail values(3, 'aaaaaaaa', 3, 'commit');
insert into trade_detail values(4, 'aaaaaaab', 1, 'add');
insert into trade_detail values(5, 'aaaaaaab', 2, 'update');
insert into trade_detail values(6, 'aaaaaaab', 3, 'update again');
insert into trade_detail values(7, 'aaaaaaab', 4, 'commit');
insert into trade_detail values(8, 'aaaaaaac', 1, 'add');
insert into trade_detail values(9, 'aaaaaaac', 2, 'update');
insert into trade_detail values(10, 'aaaaaaac', 3, 'update again');
insert into trade_detail values(11, 'aaaaaaac', 4, 'commit');
我们在执行下面插叙语句
代码语言:javascript复制mysql> select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; /*语句Q1*/
看看是否使用了索引
我们发现第一行使用索引,这个索引是建立在主键索引上的,并且扫描了一行,但是第二行,我们发现没有使用索引,进行了全表扫描。
在这个执行计划中,是从tradelog表中取tradeid字段,在到trade_detail表查询匹配字段,因此我们称tradeLog叫驱动表,而trade_detail为被驱动表.把tradeid称为关联字段
他的具体执行步骤如下
- 根据id在tradelog表找到一行
- 获取到tradeid字段的值
- 在根据tradeid的值到trade_detail表中匹配符合的行,而在explain中第二行我们发现key=null,说明是按照遍历主键索引的方式,一个个判断tradeid的值是否符合
正常按照我们的理解,第二行的tradeid它也是有索引的,应该也是使用索引才对,为什么没有使用索引的
其实第三步,相当是下面语句
代码语言:javascript复制mysql> select * from trade_detail where tradeid=$L2.tradeid.value;
其中,$L2.tradeid.value的字符集是utf8mb4。
此时我们可以想到字符集uft8mb4是uft8的超集,索引当两个字段比较的时候,会把utf8装成utf8mb4字符集,再做比较.
因此在执行上面这个语句的时候,需要被驱动表的字段一个个转换成uft8mb4,再去比较。
代码语言:javascript复制select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;
此时我们知道,如果字段使用了函数,将不会使用索引快速定位能力,这里我们就明白了,为什么被驱动表会全表扫描
作为对比我们可以下面语句看看效果
代码语言:javascript复制mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;
我们发现为什么执行计划里面的第二句使用了索引呢,我们来分析一下
依然像上面分析的一样,我们看到下面语句
代码语言:javascript复制select operator from tradelog where traideid =$R4.tradeid.value;
然后我们发现$R4.tradeid.value的字符集是utf8,此时mysql会把这个字符转成utf8mb4,因此变成了下面语句
代码语言:javascript复制select operator from tradelog where traideid =CONVERT($R4.tradeid.value USING utf8mb4);
这里看到我们使用的函数在使用在了参数上,因此此时是可以使用traideid索引。
说到了这里,我们看看如何对我们开始的sql进行优化,两种方案
- 直接把被驱动表的tradeid的字段字符改成uft8mb4
alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null 2.
- 如果数据量比较大的,可以使用下面语句,主动在驱动表转成utf8
mysql> select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;
总结:上面三个案例其实是一件事,就是对索引字段函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索能力.