二、[mysql]之Explain讲解与实战

目录

本章内容都是mysql基于5.7版本讲解。

一、了解Explain

1.Explain介绍

EXPLAIN语句提供了有关MySQL如何执行语句的信息。EXPLAIN适用于SELECT、DELETE、INSERT、REPLACE和UPDATE语句。

使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈在select语句之前增加explain关键字,MySQL会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。

注意:如果from中包含子查询,仍会执行该子查询,将结果放入临时表中

官方文档


二、Explain相关字段

首先创建了三张表,如下

演员表

sql 复制代码
 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;

这些表自己插入几条数据就行。

电影表(增加了一个单值索引)

sql 复制代码
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;

演员电影关联表(增加了一个联合索引)

sql 复制代码
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

1.partitions

5.7版本执行explain时默认是有这个一列的,但是更早之前的版本可能没有,需要增加一个PARTITIONS 关键字。

就是我们这张表是否有分区,大多数情况都是不用的,用的话也是进行分库分表不做分区。没有分区的话就是NULL。可以忽略这个字段。

sql 复制代码
旧版本:EXPLAIN PARTITIONS SELECT * FROM `actor` WHERE id = 1;

5.7之后版本:EXPLAIN  SELECT * FROM `actor` WHERE id = 1;

2.filtered

5.7版本执行explain时默认是有这个一列的,但是更早之前的版本可能没有,需要增加一个EXTENDED 关键字。

sql 复制代码
旧版本: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值小的表)。

3.SHOW WARNINGS命令

java 复制代码
EXPLAIN   SELECT * FROM `actor` WHERE id = 1;
SHOW WARNINGS;


SHOW WARNINGS执行结果。

java 复制代码
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语句优化过后的一个结果,这个结果可能不可以直接运行。

三、Explain比较重要字段

1.id

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。

id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。

2.select_type

表示对应行是简单还是复杂的查询。

  1. simple:简单查询。查询不包含子查询和union
  2. primary:复杂查询中最外层的 select
  3. subquery:包含在 select 中的子查询(不在 from 子句中)
  4. derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义)

=》sql举例=《

简单查询

sql 复制代码
EXPLAIN SELECT * FROM actor WHERE id = 1

包含DERIVED的查询

前提需要先关闭mysql5.7新特性对衍生表的合并优化。

未关闭之前

sql 复制代码
EXPLAIN SELECT (SELECT 1 FROM actor WHERE id = 1) FROM (SELECT * FROM film WHERE id = 1) der

关闭mysql5.7新特性对衍生表的合并优化

sql 复制代码
SET SESSION optimizer_switch='derived_merge=off';
sql 复制代码
EXPLAIN SELECT (SELECT 1 FROM actor WHERE id = 1) FROM (SELECT * FROM film WHERE id = 1) der

id越大越先执行,如果id值相等则按照行顺序执行。

从这个例子结合3.table看,可以看出,mysql先去查询film表中数据,然后去子查询actor表,最后进行衍生表(临时表)查询。

3.table

这一列表示 explain 的一行正在访问哪个表。

当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查

询。当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id。

例如上方例子中,其中3依赖id中的3。

4.type

这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。

依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL

一般来说,得保证查询达到range级别,最好达到ref。

  • NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可
    以单独查找索引来完成,不需要在执行时访问表。(这种情况比较少见)
    =》sql举例=《
sql 复制代码
EXPLAIN SELECT MIN(id) FROM film;

这个例子中,table列为null,表示在优化阶段分解查询语句,在执行阶段用不着再访问表或索引就能得到结果。

  • const, system:mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于
    primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是
    const的特例,表里只有一条元组匹配时为system。
    =》sql举例=《
sql 复制代码
EXPLAIN  SELECT * FROM (SELECT * FROM film WHERE id = 1) tmp;
SHOW WARNINGS;


sql 复制代码
/* select#1 */ SELECT '1' AS `id`,'电影1' AS `name` FROM DUAL
  • eq_ref:primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在
    const 之外最好的联接类型了,简单的 select 查询不会出现这种 type
    =》sql举例=《
sql 复制代码
EXPLAIN SELECT * FROM film_actor LEFT JOIN film ON film_actor.film_id = film.id

关联查询时,两张表的数据都要查询,所以id的值相同,按照第一行先执行然后第二行再执行的顺序。

  • ref:相比 eq_ref,不使用唯一索引,而是使用普通索引(二级索引)或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。
    =》sql举例1=《
sql 复制代码
EXPLAIN SELECT * FROM film WHERE NAME = 'film1';

这个sql使用到了这张表的二级索引:idx_name

=》sql举例2=《

关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。

sql 复制代码
EXPLAIN SELECT film_id FROM film LEFT JOIN film_actor ON film.id = film_actor.film_id


  • range:范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。
    =》sql举例=《
sql 复制代码
EXPLAIN SELECT * FROM actor WHERE id > 1;
  • index:扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些。

这里来讲讲覆盖索引:

假设我们有一张表user_info,有三列分别为id,name,sex。增加一个单值索引:KEY idx_name (name)

sql 复制代码
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+树数据结构长这样。

主键索引:

二级索引:

sql语句:

sql 复制代码
EXPLAIN SELECT id,NAME FROM `user_info`

在有name这列的索引时:我们只查询name和id时。在二级索引的数据结构中我们可以看到叶子节点是完全满足我们查询的内容的,而且相比于主键索引,主键索引的叶子节点的数据更多(查询数据时需要把查询的叶子节点中的数据加载到内存,主键索引占用空间更多,二级数据少占用空间少。)。所以mysql选择去二级索引里去查询。二级索引涵盖了要查询的列内容。走二级索引查询就可满足条件的这种方式即覆盖索引。覆盖索引不是一个索引,而是一种方式。

  • ALL::即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。

5.possible_keys

这一列显示查询可能使用哪些索引来查找。

explain 时可能出现 possible_keys 列有内容,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引

对此查询帮助不大,选择了全表查询。

如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提

高查询性能,然后用 explain 查看效果。

请结合上边几个例子进行理解。

6.key

这一列显示mysql实际采用哪个索引来优化对该表的访问。

如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force

index、ignore index。

请结合上边几个例子进行理解。

7.key_len

这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。
=》sql举例=《

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就更加方便了。

sql 复制代码
EXPLAIN SELECT * FROM film_actor WHERE film_id = 999 AND actor_id = 666;

这个例子就表示所有字段都用上了索引。

key_len计算规则如下:

  1. 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节
    (1)char(n):如果存汉字长度就是 3n 字节
    (2)varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串
    (3)varchar是变长字符串
  2. 数值类型
    (1)tinyint:1字节
    (2)smallint:2字节
    (3)int:4字节
    (4)bigint:8字节
  3. 时间类型
    (1)date:3字节
    (2)timestamp:4字节
    (3)datetime:8字节

如果字段允许为 NULL,需要1字节记录是否为 NULL
索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

8.ref

这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(表名 . 列名)。

sql 复制代码
EXPLAIN SELECT film_id FROM film LEFT JOIN film_actor ON film.id = film_actor.film_id;

9.rows

这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。

10.Extra

这一列展示的是额外信息。常见的重要值如下:

  • Using index:使用覆盖索引。
sql 复制代码
EXPLAIN SELECT film_id FROM film_actor WHERE film_id = 1;
  • Using where:使用 where 语句来处理结果,并且查询的列未被索引覆盖。
sql 复制代码
EXPLAIN SELECT * FROM actor WHERE NAME = 'qqqq';
  • Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围。
sql 复制代码
EXPLAIN SELECT * FROM film_actor WHERE film_id > 1;
  • Using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。
sql 复制代码
EXPLAIN SELECT DISTINCT NAME FROM actor

给改变name增加一个索引就可以解决这个问题。

之前举过的例子,film表中的name是加过索引的。

sql 复制代码
EXPLAIN SELECT DISTINCT NAME FROM film
  • Using filesort:将用外部排序而不是索引排序,数据较小时在内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。(后续会详细讲解)
sql 复制代码
EXPLAIN SELECT * FROM actor ORDER BY NAME;
sql 复制代码
EXPLAIN SELECT * FROM film ORDER BY NAME;
  • Select tables optimized away:使用某些聚合函数(比如 max、min)来访问存在索引的某个字段时。

四、索引优化实战(遵循原则)

创建表

sql 复制代码
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='员工记录表'
sql 复制代码
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());

1.全值匹配

条件中的字段要完全匹配

=》sql举例=《

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei';

这个key_len是怎么来的:name是varchar(24)。通过上边的公式,3n+2计算:3*24+2=74

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE LEFT(NAME,3)= 'LiLei';

不了解left函数的,他接受两个参数,分别为Left(str,len),str为你要截取的字符串,len为从左开始截取几位。

sql 复制代码
SELECT LEFT('LiLei',3) AS 结果

2.最左前缀法则

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

之前内容也详细讲过了,这里只举例子。

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME = 'Bill' AND age = 31;
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE age = 30 AND POSITION = 'dev';
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE POSITION = 'manager';

3.不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描

=》sql举例=《

添加索引

sql 复制代码
ALTER TABLE `employees` ADD INDEX `idx_hire_time` (`hire_time`) USING BTREE ;
sql 复制代码
EXPLAIN select * from employees where date(hire_time) ='2018‐09‐30';

DATE(日期)函数,只提取日期的年月日。

sql 复制代码
SELECT DATE('2018-09-30 :00:00:00') AS 日期

为什么改成年月日查询就不走索引呢?

原因:因为索引树里边保存的数据是年月日时分秒保存的,单独年月日是对应不上的,是没有办法完全匹配上的,导致无法走索引。

优化思路:

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE hire_time >='2018-09-30 00:00:00' 
AND hire_time <='2018-09-30 23:59:59'

演示完毕,删除索引

sql 复制代码
ALTER TABLE `employees` DROP INDEX `idx_hire_time`

4.存储引擎不能使用索引中范围条件右边的列

=》sql举例=《

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei' AND age = 22 AND POSITION ='manager';

通过ken_len可以看出都三个条件都走索引了。
=》sql举例=《

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'。

5.尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少 select * 语句

覆盖索引上边已经讲过了,为什么不推荐使用select * ,是因为你要走覆盖索引,那索引中的叶子节点不是保存的所有列数据,可能就是某几列的索引,所以尽量查询贴近二级索引相关数据。
=》sql举例=《

sql 复制代码
EXPLAIN SELECT NAME,age FROM employees WHERE NAME= 'LiLei' AND age = 23 AND POSITION ='manager';
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME= 'LiLei' AND age = 23 AND POSITION ='manager';

6.mysql在使用不等于(!=或者<>),not in ,not exists 的时候无法使用索引会导致全表扫描

< 小于、 > 大于、 <=、>= 这些,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME != 'LiHua';

通过key列可以看出没有走索引,为啥还有这种情况呢?

因为mysql走不走索引是有一套计算逻辑的,它可能认为你直接扫描全表可能比索引要快,一般情况表数据少的话,可能有种情况。

7.is null,is not null 一般情况下也无法使用索引

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME IS NULL

8.like以通配符开头('$abc...')mysql索引失效会变成全表扫描操作

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME LIKE '%Li'
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME LIKE 'Li%'

结合B+树的结构,不难看出前百分号,即后缀包含Li的字符串,这个范围中不一定在B+树是有序的,没有办法走索引。

但是后百分号,前边Li是固定,那同样都是Li开头的数据,在B+树保存的肯定是有序的,所以是能走索引的。

那如果我就需要用到前百分号,怎么能优化一下呢?那就尽量让他走覆盖索引。

sql 复制代码
EXPLAIN SELECT NAME,age,POSITION FROM employees WHERE NAME LIKE '%Li'

把type为ALL变成了index。

like的后百分号匹配相当于常量查询查询。

like KK%相当于=常量,%KK和%KK% 相当于范围

9.字符串不加单引号索引失效

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME = '1000';
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME = 1000;

10.少用or或in

用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引,详见后续博客范围查询优化内容。

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE NAME = 'LiHua' OR NAME = 'HanLei';

11.范围查询优化

添加一个索引

sql 复制代码
ALTER TABLE `employees` ADD INDEX `idx_age` (`age`) USING BTREE ;
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE age >=1 AND age <=2000;

没走索引原因:mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。比如这个例子,可能是

由于单次数据量查询过大导致优化器最终选择不走索引

优化方法:可以将大的范围拆分成多个小范围。

sql 复制代码
EXPLAIN SELECT * FROM employees WHERE age >=1 AND age <=1000;
sql 复制代码
EXPLAIN SELECT * FROM employees WHERE age >=1001 AND age <=2000;

为什么第一条没走,但是第二条走了呢?

mysql其实自己有一套怎么查询的逻辑,会对每种方案进行一个评分,最终按照分数高的方案来执行,这种情况即选择全表扫描还是走索引查询。评分相关内容在后边博客中会讲到,这里先引出来一下这块逻辑。

最后,去掉索引。

sql 复制代码
ALTER TABLE `employees` DROP INDEX `idx_age`;
相关推荐
西岭千秋雪_9 分钟前
MySQL日志梳理(存储引擎层)
java·数据库·分布式·mysql·oracle
蟹至之38 分钟前
【MySQL】索引 (下) —— 索引的类型、创建索引、删除索引
数据库·mysql·索引
某空m1 小时前
【Android】浅析DataBinding
android·开发语言
musk12121 小时前
ADB 常用命令总结(内容由 AI 生成)
adb
Yyyy4822 小时前
K8s 部署 MySQL 主从复制集群
adb
sky北城2 小时前
You are not able to choose some of the languages, because locales for them a
android
一过菜只因2 小时前
MySql学习(2)
数据库·学习·mysql
儿歌八万首2 小时前
Jetpack Compose 实战:打造高性能轮播图 (Carousel) 组件
android·前端·kotlin
QING6182 小时前
Kotlin Flow 防抖(Debounce)详解
android·kotlin·android jetpack
QING6182 小时前
Kotlin Flow 防抖(Debounce)、节流(Throttle)、去重(distinctUntilChanged) —— 新手指南
android·kotlin·android jetpack