本章内容都是mysql基于5.7版本讲解。
EXPLAIN语句提供了有关MySQL如何执行语句的信息。EXPLAIN适用于SELECT、DELETE、INSERT、REPLACE和UPDATE语句。
使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈在select语句之前增加explain关键字,MySQL会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。
注意:如果from中包含子查询,仍会执行该子查询,将结果放入临时表中
官方文档
首先创建了三张表,如下
演员表
CREATE TABLE `actor` (
`id` INT(11) NOT NULL,
`name` VARCHAR(45) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
电影表(增加了一个单值索引)
CREATE TABLE `film` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
演员电影关联表(增加了一个联合索引)
CREATE TABLE `film_actor` (
`id` INT(11) NOT NULL,
`film_id` INT(11) NOT NULL COMMENT '电影表主键',
`actor_id` INT(11) NOT NULL COMMENT '演员表主键',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '额外增加的字段',
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
5.7版本执行explain时默认是有这个一列的,但是更早之前的版本可能没有,需要增加一个PARTITIONS 关键字。
就是我们这张表是否有分区,大多数情况都是不用的,用的话也是进行分库分表不做分区。没有分区的话就是NULL。可以忽略这个字段。
旧版本:EXPLAIN PARTITIONS SELECT * FROM `actor` WHERE id = 1;
5.7之后版本:EXPLAIN SELECT * FROM `actor` WHERE id = 1;
5.7版本执行explain时默认是有这个一列的,但是更早之前的版本可能没有,需要增加一个EXTENDED 关键字。
旧版本:EXPLAIN EXTENDED SELECT * FROM `actor` WHERE id = 1;
5.7之后版本:EXPLAIN SELECT * FROM `actor` WHERE id = 1;
filtered 列,是一个半分比的值,rows *filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)。
EXPLAIN SELECT * FROM `actor` WHERE id = 1;
SHOW WARNINGS;
select '1' AS `id`,'Li' AS `name`,'2023-09-04 19:02:55' AS `update_time` from `org_47`.`actor` where 1
结果2,即mysql对sql语句优化过后的一个结果,这个结果可能不可以直接运行。
id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。
id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
表示对应行是简单还是复杂的查询。
=》sql举例=《
简单查询
EXPLAIN SELECT * FROM actor WHERE id = 1
包含DERIVED的查询
前提需要先关闭mysql5.7新特性对衍生表的合并优化。
未关闭之前
EXPLAIN SELECT (SELECT 1 FROM actor WHERE id = 1) FROM (SELECT * FROM film WHERE id = 1) der
SET SESSION optimizer_switch='derived_merge=off';
EXPLAIN SELECT (SELECT 1 FROM actor WHERE id = 1) FROM (SELECT * FROM film WHERE id = 1) der
id越大越先执行,如果id值相等则按照行顺序执行。
从这个例子结合3.table看,可以看出,mysql先去查询film表中数据,然后去子查询actor表,最后进行衍生表(临时表)查询。
这一列表示 explain 的一行正在访问哪个表。
当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查
询。当有 union 时,UNION RESULT 的 table 列的值为
例如上方例子中,其中3依赖id中的3。
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
一般来说,得保证查询达到range级别,最好达到ref。
EXPLAIN SELECT MIN(id) FROM film;
这个例子中,table列为null,表示在优化阶段分解查询语句,在执行阶段用不着再访问表或索引就能得到结果。
EXPLAIN SELECT * FROM (SELECT * FROM film WHERE id = 1) tmp;
SHOW WARNINGS;
/* select#1 */ SELECT '1' AS `id`,'电影1' AS `name` FROM DUAL
EXPLAIN SELECT * FROM film_actor LEFT JOIN film ON film_actor.film_id = film.id
关联查询时,两张表的数据都要查询,所以id的值相同,按照第一行先执行然后第二行再执行的顺序。
EXPLAIN SELECT * FROM film WHERE NAME = 'film1';
这个sql使用到了这张表的二级索引:idx_name
=》sql举例2=《
关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。
EXPLAIN SELECT film_id FROM film LEFT JOIN film_actor ON film.id = film_actor.film_id
EXPLAIN SELECT * FROM actor WHERE id > 1;
这里来讲讲覆盖索引:
假设我们有一张表user_info,有三列分别为id,name,sex。增加一个单值索引:KEY idx_name (name)
。
CREATE TABLE `user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`sex` varchar(2) DEFAULT NULL COMMENT '性别',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
按照我们第一个博客中讲到的,此时主键索引和二级索引对应的B+树数据结构长这样。
EXPLAIN SELECT id,NAME FROM `user_info`
在有name这列的索引时:我们只查询name和id时。在二级索引的数据结构中我们可以看到叶子节点是完全满足我们查询的内容的,而且相比于主键索引,主键索引的叶子节点的数据更多(查询数据时需要把查询的叶子节点中的数据加载到内存,主键索引占用空间更多,二级数据少占用空间少。)。所以mysql选择去二级索引里去查询。二级索引涵盖了要查询的列内容。走二级索引查询就可满足条件的这种方式即覆盖索引。覆盖索引不是一个索引,而是一种方式。
这一列显示查询可能使用哪些索引来查找。
explain 时可能出现 possible_keys 列有内容,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引
对此查询帮助不大,选择了全表查询。
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提
高查询性能,然后用 explain 查看效果。
请结合上边几个例子进行理解。
这一列显示mysql实际采用哪个索引来优化对该表的访问。
如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force
index、ignore index。
请结合上边几个例子进行理解。
这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。
=》sql举例=《
EXPLAIN SELECT * FROM film_actor WHERE film_id = 999;
通过key列我们知道这个查询使用到了idx_film_actor_id索引,这个索引是一个联合索引(film_id, actor_id)两个列。
都为int类型,一个int占4个字节,两个则为4+4=8个字节。
通过key_len列为4我们知道,它这个查询只用到了部分索引,也就是用到了film_id相关的索引,为什么是film_id而不是actor_id呢?这里结合上一博客的最左原理就不难知道,联合索引的优先级是先film_id在actor_id结合在一起排序的,如果要在联合索引中查询actor_id那么必须保证film_id是有顺序的,因为是actor_id是在film_id的基础上有序的。在我们不知道ken_len时通过最左原理也能推断出来,有了ken_len就更加方便了。
EXPLAIN SELECT * FROM film_actor WHERE film_id = 999 AND actor_id = 666;
key_len计算规则如下:
如果字段允许为 NULL,需要1字节记录是否为 NULL
索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(表名 . 列名)。
EXPLAIN SELECT film_id FROM film LEFT JOIN film_actor ON film.id = film_actor.film_id;
这一列是mysql估计
要读取并检测的行数,注意这个不是结果集里的行数。
这一列展示的是额外信息。常见的重要值如下:
EXPLAIN SELECT film_id FROM film_actor WHERE film_id = 1;
EXPLAIN SELECT * FROM actor WHERE NAME = 'qqqq';
EXPLAIN SELECT * FROM film_actor WHERE film_id > 1;
EXPLAIN SELECT DISTINCT NAME FROM actor
给改变name增加一个索引就可以解决这个问题。
之前举过的例子,film表中的name是加过索引的。
EXPLAIN SELECT DISTINCT NAME FROM film
EXPLAIN SELECT * FROM actor ORDER BY NAME;
EXPLAIN SELECT * FROM film ORDER BY NAME;
创建表
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表'
INSERT INTO employees(NAME,age,POSITION,hire_time) VALUES('LiHua',22,'manager',NOW());
INSERT INTO employees(NAME,age,POSITION,hire_time) VALUES('HanLei',23,'dev',NOW());
INSERT INTO employees(NAME,age,POSITION,hire_time) VALUES('Lucy',23,'dev',NOW());
条件中的字段要完全匹配
=》sql举例=《
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei';
这个key_len是怎么来的:name是varchar(24)。通过上边的公式,3n+2计算:3*24+2=74
EXPLAIN SELECT * FROM employees WHERE LEFT(NAME,3)= 'LiLei';
不了解left函数的,他接受两个参数,分别为Left(str,len),str为你要截取的字符串,len为从左开始截取几位。
SELECT LEFT('LiLei',3) AS 结果
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
之前内容也详细讲过了,这里只举例子。
EXPLAIN SELECT * FROM employees WHERE NAME = 'Bill' AND age = 31;
EXPLAIN SELECT * FROM employees WHERE age = 30 AND POSITION = 'dev';
EXPLAIN SELECT * FROM employees WHERE POSITION = 'manager';
=》sql举例=《
添加索引
ALTER TABLE `employees` ADD INDEX `idx_hire_time` (`hire_time`) USING BTREE ;
EXPLAIN select * from employees where date(hire_time) ='2018‐09‐30';
DATE(日期)函数,只提取日期的年月日。
SELECT DATE('2018-09-30 :00:00:00') AS 日期
为什么改成年月日查询就不走索引呢?
原因:因为索引树里边保存的数据是年月日时分秒保存的,单独年月日是对应不上的,是没有办法完全匹配上的,导致无法走索引。
优化思路:
EXPLAIN SELECT * FROM employees WHERE hire_time >='2018-09-30 00:00:00'
AND hire_time <='2018-09-30 23:59:59'
演示完毕,删除索引
ALTER TABLE `employees` DROP INDEX `idx_hire_time`
=》sql举例=《
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei' AND age = 22 AND POSITION ='manager';
通过ken_len可以看出都三个条件都走索引了。
=》sql举例=《
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei' AND age > 22 AND POSITION ='manager';
通过key_len可以看出只用到了索引中name和age字段,position字段没有走索引。
为什么会是这样呢?
首先,在name确定的情况下,age相对于name是有序的,position是有序的。
但是,第二条件是age大于的情况,即在age不确定的情况下,position不一定是有序的。但是age相对于确定的name下肯定是有序的。索引age能走索引,但是position是不可以的。
也就是说,在name=‘LiLei’ AND age > 22 范围下,position不是连续出现的,即不是有序的。可能age=23下有一部分内容,age24下没有,然后age30下又有了。这总情况下需要在这个范围扫描一下数据,看那些符合POSITION =‘manager’。
覆盖索引上边已经讲过了,为什么不推荐使用select * ,是因为你要走覆盖索引,那索引中的叶子节点不是保存的所有列数据,可能就是某几列的索引,所以尽量查询贴近二级索引相关数据。
=》sql举例=《
EXPLAIN SELECT NAME,age FROM employees WHERE NAME= 'LiLei' AND age = 23 AND POSITION ='manager';
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei' AND age = 23 AND POSITION ='manager';
< 小于、 > 大于、 <=、>= 这些,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引
EXPLAIN SELECT * FROM employees WHERE NAME != 'LiHua';
通过key列可以看出没有走索引,为啥还有这种情况呢?
因为mysql走不走索引是有一套计算逻辑的,它可能认为你直接扫描全表可能比索引要快,一般情况表数据少的话,可能有种情况。
EXPLAIN SELECT * FROM employees WHERE NAME IS NULL
EXPLAIN SELECT * FROM employees WHERE NAME LIKE '%Li'
EXPLAIN SELECT * FROM employees WHERE NAME LIKE 'Li%'
结合B+树的结构,不难看出前百分号,即后缀包含Li的字符串,这个范围中不一定在B+树是有序的,没有办法走索引。
但是后百分号,前边Li是固定,那同样都是Li开头的数据,在B+树保存的肯定是有序的,所以是能走索引的。
那如果我就需要用到前百分号,怎么能优化一下呢?那就尽量让他走覆盖索引。
EXPLAIN SELECT NAME,age,POSITION FROM employees WHERE NAME LIKE '%Li'
like的后百分号匹配相当于常量查询查询。
like KK%相当于=常量,%KK和%KK% 相当于范围
EXPLAIN SELECT * FROM employees WHERE NAME = '1000';
EXPLAIN SELECT * FROM employees WHERE NAME = 1000;
用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引,详见后续博客范围查询优化内容。
EXPLAIN SELECT * FROM employees WHERE NAME = 'LiHua' OR NAME = 'HanLei';
添加一个索引
ALTER TABLE `employees` ADD INDEX `idx_age` (`age`) USING BTREE ;
EXPLAIN SELECT * FROM employees WHERE age >=1 AND age <=2000;
没走索引原因:mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。比如这个例子,可能是
由于单次数据量查询过大导致优化器最终选择不走索引
优化方法:可以将大的范围拆分成多个小范围。
EXPLAIN SELECT * FROM employees WHERE age >=1 AND age <=1000;
EXPLAIN SELECT * FROM employees WHERE age >=1001 AND age <=2000;
为什么第一条没走,但是第二条走了呢?
mysql其实自己有一套怎么查询的逻辑,会对每种方案进行一个评分,最终按照分数高的方案来执行,这种情况即选择全表扫描还是走索引查询。评分相关内容在后边博客中会讲到,这里先引出来一下这块逻辑。
最后,去掉索引。
ALTER TABLE `employees` DROP INDEX `idx_age`;