索引
顾名思义,查找某物的标签,相当于书签,相当于目录。
如果我们想在一本书中快速找到特定的主题,最快的方法时查找索引,看主题在那个页码。
而对于mysql而言,如果需要查找某一行的值,可以先通过索引找对应的值,然后根据索引匹配的记录找到对应的数据行。
然而 ,有时会发现,即使查询条件有索引,查询效率依然很慢。。。当然,大多数索引对于查询速度的提升还是非常可观的,我今天要说的是有索引却不走索引的几种情况。
函数操作
很多时候我们在查询数据的时候,会“拜托”函数帮忙。我们往往关注查询结果而忽略了效率。
use mu;
drop table if exists t1; /* 如果表t1存在则删除表t1 */
CREATE TABLE `t1` ( /* 创建表t1 */
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` varchar(20) DEFAULT NULL,
`b` int(20) DEFAULT NULL,
`c` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`) USING BTREE,
KEY `idx_b` (`b`) USING BTREE,
KEY `idx_c` (`c`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
drop procedure if exists insert_t1; /* 如果存在存储过程insert_t1,则删除 */
delimiter ;;
create procedure insert_t1() /* 创建存储过程insert_t1 */
begin
declare i int; /* 声明变量i */
set i=1; /* 设置i的初始值为1 */
while(i<=10000)do /* 对满足i<=10000的值进行while循环 */
insert into t1(a,b) values(i,i); /* 写入表t1中a、b两个字段,值都为i当前的值 */
set i=i+1; /* 将i加1 */
end while;
end;;
delimiter ;
call insert_t1(); /* 运行存储过程insert_t1 */
update t1 set c = '2019-05-22 00:00:00'; /* 更新表t1的c字段,值都为'2019-05-22 00:00:00' */
update t1 set c = '2019-05-21 00:00:00' where id=10000; /* 将id为10000的行的c字段改为与其它行都不一样的数据,以便后面实验使用 */对于上面建好的表,执行如下操作(查询表t1单独某一天的所有数据):
explain select * from t1 where date(c) ='2019-05-21';可以用explain来分析某条SQL命令(语句)的效率:

type 为 ALL,key 字段结果为 NULL,因此知道该 SQL 是没走索引的全表扫描。
至于用explain分析后所得表,我们重点关注type字段、key字段、select_type字段即可。
type值(解释):
- system:查询对象只有一行数据,且只能用于MyISAM和Memory引擎的表。
- const:基于主键或唯一索引查询,最多返回一条结果。
- eq_ref:表连接时基于主键或非NULL的唯一索引完成扫描。
- ref:值查询,或者表间等值连接。
- fulltext:全文检索。
- ref_or_null:表连接类型是ref,但全文扫描的索引列中可能包含NULL值。
- index_merge:利用多个索引。
- unique_subquery:子查询中使用唯一索引。
- index_subquery:子查询中使用普通索引。
- range:利用索引进行范围查询。
- index:全索引扫描。
- ALL:全表扫描。
( 上面这些情况,查询性能从上到下依次变差! )
上图type为ALL可以得知:并没有走索引,而是全表扫描。 原因: 对条件字段做函数操作不走索引。
why:
我们知道,SQL索引使用的是B+树,这里不再多说,但是分析过此树结构后,我发现,索引树中存储的是列的实际值和主键值。如果拿‘2019-05-21’来匹配,将无法定位到索引树中的值,故而放弃索引,改走全表扫描。SQL优化建议:
因此如果需要优化的话,改成 c 字段实际值相匹配的形式。因为 SQL 的目的是查询 2019-05-21 当天所有的记录,因此可以改成范围查询,如下:
select * from t1 where c>='2019-05-21 00:00:00' and c<='2019-05-21 23:59:59';用explain分析一下:
explain select * from t1 where c>='2019-05-21 00:00:00' and c<='2019-05-21 23:59:59';根据上面执行的结果,可确定,走了 c 字段的索引(对应关注字段 key),扫描行数 1 行(对应关注字段 rows)。
经验分享: 类似求某一天或者某一个月数据的需求,建议写成类似上例的范围查询,可让查询能走索引。避免对条件索引字段做函数处理。
隐式转换
什么时候发生隐式转换?
当操作符与不同类型的操作对象一起使用时,就会发生类型转换以使操作兼容。某些转换是隐式的!
网上找到了一份MySQL文档中文版,可以看看。隐式转换估计是很多 MySQL 使用者踩过的坑,比如联系方式字段。由于有时电话号码带加、减等特殊字符,有时需要以 0 开头,因此一般设计表时会使用 varchar 类型存储,并且会经常做为条件来查询数据,所以会添加索引。
而有时遇到需要按照手机号码条件(比如 11111111111)去查询数据时,因为查询者看到条件是一串数字,而忽视表中对应手机号字段是 varchar 类型,因此写出了如下不合理的SQL:
select user_name,tele_phone from user_info where tele_phone =11111111111;实际情况下这条查询语句的效率是很低的。(怎么优化?别急,先往下看:)
我们来通过实验验证一下隐式转换是否能走索引:
实验过程分为:先创建测试表并写入数据;测试隐式转换的查询并查看执行计划;测试正常查询,再查看执行计划。
比如我们要查询 a 字段等于 1000 的值,SQL如下:
mysql> select * from t1 where a=1000;
+------+------+------+---------------------+
| id | a | b | c |
+------+------+------+---------------------+
| 1000 | 1000 | 1000 | 2019-05-22 00:00:00 |
+------+------+------+---------------------+
1 row in set (0.00 sec)explain 结果:

通过 type 这列可以看到是最差的情况 ALL(全表扫描), 通过 key 这列可以看到没走 a 字段的索引,通过 rows 这列可以看到进行了全表扫描。
不走索引的原因: a 字段类型是 varchar(20),而语句中 a 字段条件值没加单引号,导致 MySQL 内部会先把a转换成int型,再去做判断,相当于实际执行的 SQL 语句如下:
select * from t1 where cast(a as signed int) =1000;因此又回到上面说的:对索引字段做函数操作时,优化器会放弃使用索引 。
而当我们添加了单引号:

通过 type 这列,可以看到是 ref(基于普通索引的等值查询,比 ALL 性能好很多,可复习第 2 节<表3-type 各项值解释>),通过key这列,可以看到已经走了 a 字段的索引,通过rows这列可以看到通过索引查询后就扫描了一行。
因此刚才遗留问题的那个例子中的 sql 1 可以这样优化:
select user_name,tele_phone from user_info where tele_phone ='11111111111';经验分享: 在写SQL时,先看数据类型。
模糊查询
经过试验,发现 通配符不走索引 。
优化建议: 修改业务,让模糊查询必须包含条件字段前面的值,然后落到数据库的查询为
select * from t1 where a like '1111%';
+------+------+------+---------------------+
| id | a | b | c |
+------+------+------+---------------------+
| 1111 | 1111 | 1111 | 2019-05-22 00:00:00 |
+------+------+------+---------------------+
1 row in set (0.00 sec)注意: 这个优化方式必须结合业务,如果只是这样改SQL,可能会导致查询的结果不正确。
但这种写法是可以用到索引的,explain分析如下:

经验分享: 如果条件只知道中间的值,需要模糊查询去查,那就建议使用ElasticSearch或其它搜索服务器。
范围查询
在工作中因为要查询某个范围的数据而使用范围查询,但不知道有没有遇到过这种场景?明明范围查询的条件字段有索引,但是却全表扫描了。
我们拿测试表举例,比如要取出b字段1到2000范围数据,SQL 如下 :
select * from t1 where b>=1 and b <=2000;首先看下这条 SQL 的执行计划:
explain select * from t1 where b>=1 and b <=2000;发现并不能走b字段的索引。
原因: 优化器会根据检索比例、表大小、I/O块大小等进行评估是否使用索引。比如单次查询的数据量过大,优化器将不走索引。
优化建议: 降低单次查询范围,分多次查询:
经验分享: 这种范围查询而导致使用不了索引的场景经常出现,比如按照时间段抽取全量数据,每条SQL抽取一个月的;或者某张业务表历史数据的删除。遇到此类操作时,应该在执行之前对SQL做explain分析,确定能走索引,再进行操作,否则不但可能导致操作缓慢,在做更新或者删除时,甚至会导致表所有记录锁住,这是十分危险的。
计算操作
有时我们有对条件字段做计算操作的需求,在使用 SQL 查询时,就应该小心了。如下例:
explain select * from t1 where b-1 =1000;发现,对索引字段做运算将使用不了索引。
如何优化?
将计算操作放在等号后面:
explain select * from t1 where b =1000 + 1;这是发现,将计算操作放在等号后,能正常使用索引。
经验分享: 一般需要对条件字段做计算时,建议通过程序代码实现,而不是通过MySQL实现。如果在MySQL中计算的情况避免不了,那必须把计算放在等号后面。
总结
以上列出了几种条件字段有索引,但是使用不了索引的场景。因此在写 SQL 时应该注意这些点:
1、应该避免隐式转换
2、like查询不能以%开头
3、范围查询时,包含的数据比例不能太大
4、不建议对条件字段做运算及函数操作
















