参考资料:
《explain | 索引优化的这把绝世好剑,你真的会用吗?》
《一张图彻底搞懂MySQL的 explain》
《MySQL 性能优化神器 Explain 使用分析》
《MySQL索引应用篇:建立索引的正确姿势与使用索引的最佳指南!》
《MySQL索引失效的情况》
相关文章:
《MySQL:基础架构与存储引擎》
《MySQL:索引(1)原理与底层结构》
《MySQL:索引(2)使用与相关建议》
《mysql之慢sql与pt-query-digest》
写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
前言
在MySQL专栏之前的文章中,我们介绍了MySQL的主从复制与高可用架构MHA等,这些都是架构层面的MySQL优化方案(包括还在计划中的分库分表),今天我们再重新从最基础的SQL使用的角度去看如何优化我们的SQL。
SQL本身的优化主要是两种情况,一种是项目开发阶段的表相关的设计,另一种是项目上线后针对暴露出的问题进行优化。前一种情况需要设计好表的字段、索引,需要遵循哪些范式,是否需要冗余一些字段来减轻表连接的频率,后一种情况则需要统计慢SQL(对于慢SQL的统计与分析可以看我的这篇文章《mysql之慢sql与pt-query-digest》),然后进行系统性的分析与优化。
本文我们主要针对索引与sql语句的优化进行下讲解,深入理解本文需要对MySQL的索引结构有基本的了解,如果不熟悉的朋友可以先从我之前的文章开始看起(《索引(1)》、《索引(2)》),本文也可以看做是对这两篇文章的一些补充与完善。(本文默认使用InnoDB存储引擎)
目录
前言
一、Explain分析语句
1、id
id相同
id不同
id相同和不同都有
2、select_type
SIMPLE
PRIMARY 和 SUBQUERY
DERIVED
UNION 和 UNION RESULT
3、table
4、type
const
eq_ref
ref
range
index
ALL
5、key
6、key_len
7、Extra
Using filesort
Using index
Using temporary
Using where
using index condition
二、各类索引的注意点
1、主键索引
2、复合索引
3、前缀索引
4、唯一索引
三、索引失效的情况
1、or分割开的条件
2、模糊查询中like以%开头导致索引失效
3、参与函数运算或类型转换
3.1、隐式类型转换
3.2、字符集不一致
3.3、参与计算或内置函数
4、违背最左前缀原则
5、MySQL认为全表更快
5.1、is null 和 is not null
5.2、in 和 not in
一、Explain分析语句
explain关键字可以模拟MySQL优化器执行SQL语句,可以很好的分析SQL语句或表结构的性能瓶颈。explain用法十分简单, 在语句前加上explain就可以了, 例如:
explain select * from test where id = 1;
explain共有12列,具体的列名字如下图
下面我们会针对其中比较重要的列做一些解释。下文主要参考自《explain | 索引优化的这把绝世好剑,你真的会用吗?》
1、id
该列的值是select查询中的序号,一条简单的sql只会有1个id,这个id就是1。但是当有子查询或者连接查询等复杂查询时,sql内部会将他们拆分成多条sql进行执行,由于执行顺序的不同,就会出现多个序号,比如:1、2、3、4等,它决定了表的执行顺序。某条sql的执行计划中一般会出现三种情况:id相同、id不同,id相同和不同的都有。
id相同
例如下文的连接查询,我们看到执行结果中的两条数据id都是1,是相同的。这种情况表的执行顺序就是从上到下执行,先执行表t1,再执行表t2(每条记录涉及的表由table代表)。
explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id
id不同
如下的子查询中,我们看到执行结果中两条数据的id不同,第一条数据是1,第二条数据是2,此时序号大的先执行,这里会从下到上执行,先执行表t2,再执行表t1。
explain select * from test1 t1 where t1.id =
(select id from test1 t2 where t2.id=2);
id相同和不同都有
当sql语句变得更复杂后,就会出现id相同和不同都有的情况,此时先执行序号大的,先从下而上执行。遇到序号相同时,再从上而下执行。所以这个列子中表的顺序顺序是:test1、t1。至于<derived2>,这个是from子句中的子查询,该例中就是t2。
explain
select t1.* from test1 t1
inner join (select max(id) mid from test1 group by id) t2
on t1.id=t2.mid
2、select_type
select_type表示查询的类型,常见的如下
类型 | 含义 |
---|---|
SIMPLE | 简单SELECT查询,不包含子查询和UNION |
PRIMARY | 复杂查询中的最外层查询,表示主要的查询 |
SUBQUERY | SELECT或WHERE列表中包含了子查询 |
DERIVED | FROM列表中包含的子查询,即衍生 |
UNION | UNION关键字之后的查询 |
UNION RESULT | 从UNION后的表获取结果集 |
SIMPLE
无子查询和union的查询(但可以使用连接),这里复用下上文介绍id相同的sql。
explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id
PRIMARY 和 SUBQUERY
当出现子查询时,PRIMARY 和 SUBQUERY分别用来表示外层的复查询和内层的子查询。这里复用了id不同的sql,这里可以根据id的顺序看出是先执行的子查询,后执行的父查询。
explain select * from test1 t1 where t1.id =
(select id from test1 t2 where t2.id=2);
DERIVED
当出现from子查询时(即将一条sql的查询结果作为一张表的情况)。
explain
select t1.* from test1 t1
inner join (select max(id) mid from test1 group by id) t2
on t1.id=t2.mid
UNION 和 UNION RESULT
当使用union联合两张表的查询结果时,union后跟着的sql的查询类型为union,前一张表则和子查询类似,被标记为PRIMARY。<union1,2>表示id=1和id=2的表union,其结果被标记为UNION RESULT。(注意union result的id列为空)
explain
select * from test1
union
select* from test2
3、table
该列的值表示输出行所引用的表的名称,比如前面的:test1、test2等。不过也有特殊情况,如from子查询等。
- <unionM,N>:具有和id值的行的M并集N。
- <derivedN>:用于与该行的派生表结果id的值N。派生表可能来自(例如)from子句中的子查询 。
- <subqueryN>:子查询的结果,其id值为N。
4、type
type 字段比较重要, 它提供了判断查询是否高效的重要依据依据. 通过type字段, 我们判断此次查询是全表扫描还是索引扫描等。
执行结果从最好到最坏的的顺序是从上到下,一般掌握以下几个即可 const > eq_ref > ref > range > index > ALL。(system这种类型要求数据库表中只有一条数据,是const类型的一个特例,一般情况下是不会出现的)
假设我们现在有一张表test2包含(id,code,name)三列,有一个主键索引id与普通索引code。
const
表示通过索引一次就找到了,const用于比较primary key或uique索引,因为只匹配一行数据,所以很快,如主键置于where列表中,MySQL就能将该查询转换为一个常量。
explain select * from test2 where id=1;
eq_ref
此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果(一般见于唯一性索引的扫描)。 并且查询的比较操作通常是 =
, 查询效率较高。
explain select * from test2 t1 inner join test2 t2 on t1.id=t2.id;
ref
常用于非(主键和唯一)索引扫描,虽然命中了索引列,但由于非唯一性,在找到所需的数据后仍然需要向后继续查找。
explain select * from test2 where code = '001';
range
常用于对索引列的范围查询,比如:between ... and 或 In 等操作,执行sql如下:
explain select * from test2 where id between 1 and 2;
index
当不使用where限制查询条件,且查询的列只有索引列时,会扫描整个索引树(但因为只查询索引列因此不会回表),将该列索引全部取出。
explain select code from test2;
ALL
全表扫描,最差的一种情况。
5、key
key表示该列表示实际用到的索引(前一列possible_keys表示可能用到的索引)。
explain select * from test1 t1 where t1.id =
(select id from test1 t2 where t2.id=2);
6、key_len
表示查询优化器使用了索引的字节数. 这个字段可以评估组合索引是否完全被使用, 或只有最左部分字段被使用到。当我们使用复合索引时,只匹配到前n个和匹配到前n+1个列时的key_len是不同的。
这里举个例子,如下图中有一个复合索引涉及三个字段name,status,address,我们分别试验命中全部索引和只命中第一个索引,可以发现key_len的长度是不同的。
7、Extra
该字段包含有关MySQL如何解析查询的其他信息。
Using filesort
当 Extra 中有 Using filesort 时, 表示 MySQL 需额外的排序操作, 不能通过索引顺序达到排序效果。 一般有 Using filesort, 都建议优化去掉, 因为这样的查询 CPU 资源消耗大。
当test1表有(code,name)这样的联合索引时,我们使用全扫描索引,只查询code,但是我们对name进行逆排序,此时因为与复合索引中name的顺序不同,因此只能先将数据查询到内存中,然后再进行排序。
explain select code from test1 order by name desc;
Using index
表示是否用了覆盖索引,即走了复合索引,且不需要回表。这里不单独举例了,Using filesort样例中的sql即满足该条件。
Using temporary
使用了临时表保存中间结果,MySQL在对结果排序时使用临时表,常见于排序order by 和分组查询group by。
Using where
表示使用了where子句查询,通常表示没使用索引。
Using index condition
使用到了索引下推,该内容我们下文将介绍。
二、各类索引的注意点
1、主键索引
主键索引在存储数据时,表数据和索引数据是一起存放的。同时,MySQL默认的索引结构是B+Tree,也就代表着索引节点的数据是有序的。
如果使用UUID或者其他字符串类型的字段作为主键,那么每当插入一条新数据,都有可能破坏原本的树结构,甚至可能导致磁盘块中的数据挪动,比如分页,这些都是严重影响性能的。因此一般我们会建议使用自增ID作为主键,当有新数据要插入时数据都会放到最后。
2、复合索引
由于复合索引内部是按照列1、列2、列3这样的顺序存储的,所以当使用时如果产生了跳跃,如使用了第一和第三个索引列,那么将只有第一列时生效的(索引列在where中的前后顺序不会影响是否生效,因为优化器会自己调整)。因此我们在建立复合索引时需要确认好查询频率,尽量将频率高的列放在前面,才能最大程度的将复合索引利用起来。
另外,由于非主键索引只存储主键的值,因此如果查询的列中出现了非该复合索引中的列,就需要再拿该主键索引的值去主键索引树上查找数据行,这一过程就是回表。
因此为了减少回表,尽量使用覆盖索引(索引列完全包含查询列),尽量不要使用select *。同时我们可以在复合索引列中增加查询频率较高的列,以此来提高效率。
3、前缀索引
前缀索引的特点是短小精悍,我们可以利用一个字段的前N个字符创建索引,以这种形式创建的索引也被称之为前缀索引,相较于使用一个完整字段创建索引,前缀索引能够更加节省存储空间,当数据越多时,带来的优势越明显。
不过前缀索引虽然带来了节省空间的好处,但也正由于其索引节点中,未存储一个字段的完整值,所以MySQL也无法通过前缀索引来完成ORDER BY、GROUP BY等分组排序工作,同时也无法完成覆盖扫描等操作。
4、唯一索引
唯一索引有个很大的好处,就是查询数据时会比普通索引效率更高,因为基于普通索引的字段查询数据,例如:
explain select * from test2 where code = '001';
假设code 字段上建立了一个普通索引,此时基于这个字段查询数据时,当查询到一条code = "001"的数据后,此时会继续走完整个索引树,因为可能会存在多条字段值相同的数据。但如果code 字段上建立的是唯一索引,当找到一条数据后就会立马停下检索,因此本身建立唯一索引的字段值就具备唯一性。
因此唯一索引查询数据时,会比普通索引快上一截,但插入数据时就不同了,因为要确保数据不重复,所以插入前会检查一遍表中是否存在相同的数据。但普通索引则不需要考虑这个问题,因此普通索引的数据插入会快一些。
三、索引失效的情况
注意,由于当数量不同时MySQL优化器的判断是截然不同的,因此下文的内容均以数据量不少于几千条记录的情况为例。(数据量过少走不走索引其实效率影响不大,有时甚至全表扫描会更快)
建立一张用户表
CREATE DATABASE IF NOT EXISTS `test` DEFAULT CHARACTER SET utf8;
USE `test`;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint NOT NULL DEFAULT 0 COMMENT '主键,用户唯一id',
`user_name` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
`email` varchar(32) NOT NULL DEFAULT '' COMMENT '邮箱',
`phone_number` varchar(16) NOT NULL DEFAULT '' COMMENT '电话号码',
`avatar` varchar(256) NOT NULL DEFAULT '' COMMENT '头像',
`create_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '用户账号创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '上次更新记录时间',
`last_login_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '上次登录时间',
`status` int(2) NOT NULL DEFAULT 0 COMMENT '用户状态 0-正常 1-封禁',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '用户信息表';
创建存储过程,插入10万条测试数据
DROP PROCEDURE if exists insert_t_user_test;
DELIMITER $$
CREATE PROCEDURE insert_t_user_test(IN loop_times INT)
BEGIN
DECLARE var INT DEFAULT 0;
WHILE var < loop_times DO
SET var = var + 1;
INSERT INTO `t_user` VALUES (var, CONCAT('rkyao-', var), '123456', 'rkyao@163.com', '15251831704', 'avatar.jpg', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0);
END WHILE;
COMMIT;
END $$
CALL insert_t_user_test(100000);
1、or分割开的条件
用or分割开的条件, 如果or前后的列有一个没有索引,那即使另一列有索引也无法命中。 如下例中,只有user_name或email列有索引,则不会走索引。
如果两列都有索引,则可以走
这里再补充下,两列都有索引,但使用的and,则只会选择一颗索引树进行查询
2、模糊查询中like以%开头导致索引失效
由于索引的匹配类似二叉树查找(B+树也是由二叉树演化来的),比如要找‘abc’,如果是%bc,一开始的根都找不到了,自然没办法利用到索引树。不过以%结尾的还是可以使用到索引树的。
3、参与函数运算或类型转换
3.1、隐式类型转换
索引字段为字符串类型,由于在查询时,没有对字符串加单引号,MySQL的查询优化器,会自动的进行类型转换,造成索引失效。
3.2、字符集不一致
这个此前专门有过一篇讲解,可以看这里《mysql字符集不一致引起的索引失效问题》
3.3、参与计算或内置函数
字段使用函数会让优化器无从下手,B树中的值和函数的结果可能不搭边,所以不会使用索引,即索引失效。
4、违背最左前缀原则
这一点是对复合索引而言的,因为复合索引的物理结构如下,我们可以看出叶子节点的顺序是按照列1、列2这样来的,因此当我们跳过了列1,自然就无法使用到列2了。不过这一情况在8.0得到了改变,这个我们会在后续的内容中介绍跳跃式扫描。
补充:这里需要补充的是,最左前缀原则的限制里,是不允许使用范围查询的,即如果对第一个索引使用了例如<、>或者like 以%结尾都会导致该列后续的索引失效。如下图中第二例,第三列address就不会生效。
第二例中的查询过程如下:
- 利用联合索引中的name、status字段找出复合这个条件的所有索引树中的叶子节点。
- 返回索引节点存储的值给Server层,然后去逐一做回表扫描。
- 在Server层中根据address="北京市"这个条件逐条判断,最终筛选到满足条件的数据。
可以看出这里之所以没办法利用到第三列索引时因为引擎层未对数据进行判断,而是把根据前两列索引查询出来的结果交给了server层去做判断。所以第三列没有用到索引。
可能有朋友会想到,那如果在引擎层就对他们进行判断不就能利用到索引了吗,其实这一点MySQL也想到了,所以在5.6之后加入了一个新的机制叫索引下推,我们会在后续的文章中介绍。
这里给出一份参考样例,可以对照着进行深入理解。
5、MySQL认为全表更快
MySQL中决定使不使用某个索引执行查询的依据就是成本够不够小,具体是否走索引这一点我请教过我司的DBA,得到的结论也只有具体场景具体分析,没有特定失效或生效的说法。(注意这里分析的是数据较多的场景,数据较少时优化器的表现是不同的,但这里不会去分析这种场景,因为意义不大)。具体的分析可以看这篇文章《MySQL中 IS NULL、IS NOT NULL、不等于, 能用上索引吗?》
5.1、is null 和 is not null
5.2、in 和 not in
本文我们介绍了如何分析SQL语句的执行情况(通过explain语句),以及常见索引使用时的注意点与失效场景,下文我们将介绍MySQL针对查询进行的自我优化与我们如何调整SQL语句来进行优化。