我们收集了慢查询语句后,就要对语句进行分析。一条查询语句在经过 MySQL 查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划,这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。EXPLAIN 语句来帮助我们查看某个查询语句的具体执行计划,我们需要搞懂 EXPLAIN 的各个输出项代表什么意思,从而针对性的提升我们查询语句的性能。

为了下面的演示,我们先来创建一些表:

  1. CREATE TABLE `order_exp` (
  2. `id` bigint(255) NOT NULL AUTO_INCREMENT COMMENT '订单主键',
  3. `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单号',
  4. `order_note` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单描述',
  5. `insert_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '订单插入时间',
  6. `expire_duration` bigint(255) NOT NULL COMMENT '订单的过期时长,单位秒',
  7. `expire_time` datetime(0) NOT NULL COMMENT '订单过期时间',
  8. `order_status` smallint(6) NOT NULL COMMENT '订单的状态,0:未支付;:已支付;-1:已过期,关闭',
  9. PRIMARY KEY (`id`) USING BTREE,
  10. UNIQUE INDEX `u_idx_day_status`(`insert_time`, `order_status`, `expire_time`) USING BTREE,
  11. INDEX `idx_order_no`(`order_no`) USING BTREE,
  12. INDEX `idx_expire_time`(`expire_time`) USING BTREE
  13. ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

‘order_exp’ 表有另外三个派生表,’order_exp_cut’,’s1’,’s2’,表结构基本一致,有少许差别:order_exp_cut 表则是把某 order_exp 表中的列改为允许位 null,s1 和 s2 表则把二级索引 ‘expire_time’ 换成二级索引 ‘insert_time’

CREATE TABLE `order_exp_cut`  (
  `id` bigint(255) NOT NULL AUTO_INCREMENT COMMENT '订单主键',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号',
  `order_note` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单描述',
  `insert_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '订单插入时间',
  `expire_duration` bigint(255) NOT NULL COMMENT '订单的过期时长,单位秒',
  `expire_time` datetime(0) NOT NULL COMMENT '订单过期时间',
  `order_status` smallint(6) NOT NULL COMMENT '订单的状态,0:未支付;:已支付;-1:已过期,关闭',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `u_idx_day_status`(`insert_time`, `order_status`, `expire_time`) USING BTREE,
  INDEX `idx_order_no`(`order_no`) USING BTREE,
  INDEX `idx_expire_time`(`expire_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

CREATE TABLE `s1`  (
  `id` bigint(255) NOT NULL AUTO_INCREMENT COMMENT '订单主键',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单号',
  `order_note` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单描述',
  `insert_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '订单插入时间',
  `expire_duration` bigint(255) NOT NULL COMMENT '订单的过期时长,单位秒',
  `expire_time` datetime(0) NOT NULL COMMENT '订单过期时间',
  `order_status` smallint(6) NOT NULL COMMENT '订单的状态,0:未支付;:已支付;-1:已过期,关闭',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `u_idx_day_status`(`insert_time`, `order_status`, `expire_time`) USING BTREE,
  INDEX `idx_order_no`(`order_no`) USING BTREE,
  INDEX `idx_insert_time`(`insert_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

CREATE TABLE `s2`  (
  `id` bigint(255) NOT NULL AUTO_INCREMENT COMMENT '订单主键',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单号',
  `order_note` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单描述',
  `insert_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '订单插入时间',
  `expire_duration` bigint(255) NOT NULL COMMENT '订单的过期时长,单位秒',
  `expire_time` datetime(0) NOT NULL COMMENT '订单过期时间',
  `order_status` smallint(6) NOT NULL COMMENT '订单的状态,0:未支付;:已支付;-1:已过期,关闭',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `u_idx_day_status`(`insert_time`, `order_status`, `expire_time`) USING BTREE,
  INDEX `idx_order_no`(`order_no`) USING BTREE,
  INDEX `idx_insert_time`(`insert_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

执行计划的语法

执行计划的语法其实非常简单:在 SQL 查询的前面加上 EXPLAIN 关键字就行。比如:

EXPLAIN select * from table1

重点的就是 EXPLAIN 后面你要分析的 SQL 语句,除了以 SELECT 开头的查询语句,其余的 DELETE、INSERT、REPLACE 以及 UPOATE 语句前边都可以加上 EXPLAIN,用来查看这些语句的执行计划,不过我们这里对 SELECT 语句更感兴趣,所以后边只会以 SELECT 语句为例来描述 EXPLAIN 语句的用法。

我们把 EXPLAIN 语句输出的各个列的作用先做一个大概的了解:

EXPLAIN select * from order_exp

image.png

  • id: 在一个大的查询语句中每个 SELECT 关键字都对应一个唯一的 id
  • select_type: SELECT 关键字对应的那个查询的类型
  • table:查询的表名
  • partitions:匹配的分区信息
  • type:针对单表的访问方法
  • possible_keys:可能用到的索引key:实际上使用的索引
  • key_len:实际使用到的索引长度
  • ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息
  • rows:预估的需要读取的记录条数
  • filtered:某个表经过搜索条件过滤后剩余记录条数的百分比
  • Extra:—些额外的信息

执行计划详解

为了方便讲述,我们可能会适当调整对列的讲解顺序,不会完全按照 EXPLAIN 语句输出列顺序来讲解,废话不多说,我们直接开始。

table

不论我们的查询语句有多复杂,里边包含了多少个表,到最后也是需要对每个表进行单表访问的,MySQL 规定 EXPLAIN 语句输出的每条记录都对应着某个单表的访问方法,该条记录的 table 列代表着该表的表名。

id

我们知道我们写的查询语句一般都以 SELECT 关键字开头,比较简单的查询语句里只有一个 SELECT 关键字,就算是稍微复杂一点的如连表查询也是只有一个 SELECT 关键字。但是下边两种情况下在一条查询语句中会出现多个 SELECT 关键字:

  1. 查询中包含子查询的情况比如下边这个查询语句中就包含 2 个 SELECT 关键字: SELECT FROM s1 WHERE id IN ( SELECT FROM s2)
  2. 查询中包含 UNION 语句的情况,比如下边查询语句中也包含 2 个 SELECT 关键字:

SELECT FROM s1 UNION SELECT FROM s2

查询语句中每出现一个 SELECT 关键字,MySQL 就会为它分配一个唯一的 id 值。这个
id 值就是 EXPLAIN 语句的第一个列。

单 SELECT 关键字

比如下边这个查询中只有一个 SELECT 关键字,所以 EXPLAIN 的结果中也就只有一条 id 列为 1 的记录:

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’

image.png

连接查询

对于连接查询来说,一个 SELECT 关键字后边的 FROM 子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的 id 值都是相同的

EXPLAIN SELECT * FROM s1 INNER JOIN s2

image.png

包含子查询

对于包含子查询的查询语句来说,就可能涉及多个 SELECT 关键字,所以在包含子查询的查询语句的执行计划中,每个 SELECT 关键字都会对应一个唯一的 id 值,比如这样:

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = ‘a’

image.png

但是这里大家需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好了,比如说:

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE order_no = ‘a’)

image.png

包含 UNION 子句

对于包含 UNION 子句的查询语句来说,每个 SELECT 关键字对应一个 id 值也是没错的,不过还是有点儿特别的东西,比方说下边这个查询:

EXPLAIN SELECT FROM s1 UNION SELECT FROM s2

image.png

这个语句的执行计划为什么会有三条记录?原因是 UNION 子句会把多个查询的结果集合并起来并对结果集中的记录进行去重,MySQL 使用的是内部的临时表。正如上边的查询计划中所示,UNION 子句是为了把 id 为 1 的查询和 id 为 2 的查询的结果集合并起来并去重,所以在内部创建了一个名为 的临时表(就是执行计划第三条记录的 table 列的名称),id 为 NULL 表明这个临时表是为了合并两个查询的结果集而创建的。

跟 UNION 对比起来,UNION ALL 就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含 UNION ALL 子句的查询的执行计划中,就没有那个 id 为 NULL 的记录,如下所示:

EXPLAIN SELECT FROM s1 UNION ALL SELECT FROM s2

image.png

select_type

通过上边的内容我们知道,一条大的查询语句里边可以包含若干个 SELECT 关键字,每 个 SELECT 关键字代表着一个小的查询语句,而每个 SELECT 关键字的 From 子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个 SELECT 关键字中的表来说,它们的 id 值是相同的。

MySQL 为每一个 SELECT 关键字代表的小查询都定义了一个称之为 select_type 的属性,意思是我们只要知道了某个小查询的 select_type 属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,select_type 取值如下:

  • SIMPLE:简单的 select 查询,不使用 union 及子查询
  • PRIMARY:最外层的 select 查询
  • UNION:UNION 中的第二个或随后的 select 查询,不依赖于外部查询的结果集
  • UNION RESULT:UNION 结果集
  • SUBQUERY:子查询中的第一个 select 查询,不依赖于外部查询的结果集
  • DEPENDENT SUBQUERY:子查询中的第一个 select 查询,依赖于外部查询的结果集
  • DEPENDENT UNION:UNION 中的第二个或随后的 select 查询,依赖于外部查询的结果集
  • DERIVED: 用于 FROM 子句里有子查询的情况。 MySQL 会 递归执行这些子查询,把结果放在临时表里。
  • MATERIALIZED:物化子查询
  • UNCACHEABLE SUBQUERY:结果集不能被缓存的子查询,必须重新为外层查询的每一行进行评估,出现极少。
  • UNCACHEABLE UNION:UNION 中的第二个或随后的 select 查询,属于不可缓存的子查询,出现极少。

SIMPLE

简单的 select 查询,查询中不包含子查询或者 UNION

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’

image.png

连接查询也算是 SIMPLE 类型

EXPLAIN SELECT * FROM s1 INNER JOIN s2

image.png

PRIMARY

对于包含 UNION、UNION ALL 或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的 select_type 值就是 PRIMARY,比方说:

EXPLAIN SELECT FROM s1 UNION SELECT FROM s2

image.png
从结果中可以看到,最左边的小查询 SELECT * FROMN s1 对应的是执行计划中的第一条记录,它的 select_type 值就是 PRIMARY。

UNION

对于包含 UNION 或者 UNION ALL 的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的查询的 select_type 值就是 UNION,可以对比上一个例 子的效果。
image.png

UNION RESULT

MySQL 选择使用临时表来完成 UNION 查询的去重工作,针对该临时表的查询的
select_type 就是 UNION RESULT,例子上边有。
image.png

SUBQUERY

如果包含子查询的查询语句不能够转为对应的 semi-join 的形式,且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个 SELECT 关键字代表的那个查询的 select_type 就是 SUBQUERY,比如下边这个查询 :

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = ‘a’

image.png
可以看到,外层查询的 select_type 就是 PRTIMARY,子查询的 select_type 就是SUBOUERY。需要大家注意的是,由于 select_type 为 SUBQUERY 的子查询由于会被物化,所以只需要执行一遍。

上面出现了几个比较陌生的名词,这里稍作解释

semi-join:半连接优化技术,本质上是把子查询上拉到父查询中,与父查询的表做 join 操作。关键词是“上拉”。对于子查询,其子查询部分相对于父表的每个符合条件的元组,都 要把子查询执行一轮。效率低下。用半连接操作优化子查询,是把子查询上拉到父查询中, 这样子查询的表和父查询中的表是并列关系,父表的每个符合条件的元组,只需要在子表中 找符合条件的元组即可。简单来说,就是通过将子查询上拉对父查询中的数据进行筛选,以 使获取到最少量的足以对父查询记录进行筛选的信息就足够了。 子查询物化:子查询的结果通常缓存在内存或临时表中。 关联/相关子查询:子查询的执行依赖于外部查询。多数情况下是子查询的 WHERE 子 句中引用了外部查询的表。自然“非关联/相关子查询”的执行则不依赖与外部的查询。

DEPENDENT UNION、DEPENDENT SUBQUERY

在包含 UNION 或者 UNION ALL 的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的 select_type 的值就是 DEPENDENT UNION。比方说下边这个查询:

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE id = 716 UNION SELECT id FROM s1 WHERE id = 718)

image.png

这个查询比较复杂,大查询里包含了一个子查询,子查询里又是由 UNION 连起来的两个小查询。从执行计划中可以看出来,SELECT id FROM s2 WHERE id = 716 这个小查询由于是子查询中第一个查询,所以它的 select_type 是 OEPENDENT SUBOUERY。

SELECT id FROM s1 WHERE id = 718 这个查询的 select_type 就是 DEPENDENT UNION。是不是很奇怪这条语句并没有依赖外部的查询?MySQL 优化器对 IN 操作符的优化会将 IN 中的非关联子查询优化成一个关联子查询。我们可以在执行上面那个执行计划后,马上执行 show warnings\G,可以看到 MySQL 对 SQL 语句的大致改写情况

DERIVED

对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的 select_type 就是 DERIVED。

EXPLAIN SELECT FROM (SELECT id, count() as c FROM s1 GROUP BY id) AS derived_s1 where c >1

image.png
从执行计划中可以看出, id 为 2 的记录就代表子查询的执行方式,它的 select_type 是DERIVED ,说明该子查询是以物化的方式执行的。id 为 1 的记录代表外层查询,大家注意看它的 table 列显示的是 ,表示该查询是针对将派生表物化之后的表进行查询的。

MATERIALIZED

当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的 select_type 属性就是 MATERIALIZED,比如下边这个查询︰

EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2)

image.png
执行计划的第三条记录的 id 值为 2,从它的 select_type 值为 MATERIALIED 可以看出,查询优化器是要把子查询先转换成物化表。然后看执行计划的前两条记录的 id 值都为 1,说明这两条记录对应的表进行连接查询,需要注意的是第二条记录的 table 列的值是,说明该表其实就是 id 为 2 对应的子查询执行之后产生的物化表,然后将 s1 和该物化表进行连接查询。

UNCACHEABLE SUBQUERY、UNCACHEABLE UNION

出现极少,不做深入讲解,比如

explain select * from s1 where id = ( select id from s2 where order_no=@@sql_log_bin)

image.png

type

我们前边说过执行计划的一条记录就代表着 MySQL 对某个表的执行查询时的访问方法/访问类型,其中的 type 列就表明了这个访问方法/访问类型是个什么东西,是较为重要的一个指标,结果值从最好到最坏依次是:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

出现比较多的是 system>const>eq_ref>ref>range>index>ALL,一般来说,得保证查询至少达到 range 级别,最好能达到 ref。

system

当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的(INNODB 存储引擎不可能达到 system 级别),比如 MyISAM、Memory,那么对该表的访问方法就是 system以下是以 MyISAM 作为存储引擎的表:

explain select * from test_myisam

image.png

const

当我们根据主键或者唯一的二级索引列与常数进行等值匹配时,对单表的访问方法就是 const。因为只匹配一行数据,所以很快。例如将主键置于 where 列表中:

EXPLAIN SELECT * FROM s1 WHERE id = 716

image.png

B+ 树叶子节点中的记录是按照索引列排序的,对于的聚簇索引来说,它对应的 B+树叶子节点中的记录就是按照 id 列排序的。B+ 树矮而胖,所以这样根据主键值定位一条记录的速度很快。类似的,我们根据唯一二级索引列来定位一条记录的速度也很快的。

MySQL 把这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为:const,意思是常数级别的,代价是可以忽略不计的。不过这种 const 访问方法只能在主键列或者唯一二级索引列和一个常数进行等值比较时才有效,如果主键或者唯一二级索引是由多个列构成的话,组成索引的每一个列都是与常数进行等值比较时,这个 const 访问方法才有效。对于唯一二级索引来说,查询该列为 NULL 值的情况比较特殊,因为唯一二级索引列并不限制 NULL 值的数量,所以上述语句可能访问到多条记录,也就是说 is null 不可以使用 const 访问方法来执行。

eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访
问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是 eq_ref,比如说:

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id

image.png
从执行计划的结果中可以看出,MySQL 打算将 s2 作为驱动表,s1 作为被驱动表,重点关注 s1 的访问方法是 eq_ref,表明在访问 s1 表的时候可以通过主键的等值匹配来进行访问

ref

当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是 ref。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他属于查找和扫描的混合体

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’

image.png
由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以 MySQL 可能选择使用索引而不是全表扫描的方
式来执行查询。ref 访问方法比 const要差些,但是在二级索引等值比较时匹配的记录数较少时的效率还是很高的(如果匹配的二级索引记录太多那么回表的成本就太大了)。不过需要注意下边两种情况:

  1. 二级索引列值为 NULL 的情况:不论是普通的二级索引,还是唯一二级索引,它们的索引列对包含 NULL 值的数量并不限制,所以我们采用 key IS NULL 这种形式的搜索条件最多只能使用 ref 的访问方法,而不是 const 的访问方法

  2. 对于某个包含多个索引列的二级索引来说,只要是最左边的连续索引列是与常数的等值比较就可能采用 ref 的访问方法,比方说下边这几个查询:

    SELECT FROM order_exp WHERE insert_time = ‘2021-03-22 18:28:23’ SELECT FROM order_exp WHERE insert_time = ‘2021-03-22 18:28:23’ AND order_status = 0

但是如果最左边的连续索引列并不全部是等值比较的话,它的访问方法就不能称为 ref 了,比方说这样:

SELECT * FROM order_exp WHEREinsert_time = ‘2021-03-22 18:28:23’ AND order_status > -1

ref_or_null

有时候我们不仅想找出某个二级索引列的值等于某个常数的记录,还想把该列的值为 NULL 的记录也找出来,就像下边这个查询:

explain SELECT * FROM order_exp_cut WHERE order_no= ‘abc’ OR order_no IS NULL

image.png
这个查询相当于先分别从 order_exp_cut 表的 idx_order_no 索引对应的 B+树中找出
order_no IS NULLorder_no= ‘abc’ 的两个连续的记录范围,然后根据这些二级索引记录中的 id 值再回表查找完整的用户记录。

index_merge

一般情况下对于某个表的查询只能使用到一个索引,在某些场景下可以使用索引合并的方式来执行查询:

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’ OR insert_time = ‘2021-03-22 18:36:47’

image.png

unique_subquery

类似于两表连接中被驱动表的 eg_ref 访问方法,unique _subquery 是针对在一些包含 IN 子查询的查询语句中,如果查询优化器决定将 IN 子查询转换为 EXISTS 子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的 type 列的值就是unique_subquery,比如下边的这个查询语句:

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 where s1.insert_time = s2.insert_time) OR order_no = ‘a’

image.png

index_subquery

index_subquery 与 unique_subquery 类似,只不过访问子查询中的表时使用的是普通的二级索引

EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.insert_time = s2.insert_time) OR order_no = ‘a’;

range

如果使用索引获取某些范围区间的记录,那么就可能使用到 range 访问方法,一般就是在你的 where 语句中出现了 between、<、>、in 等的查询。这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。

EXPLAIN SELECT * FROM s1 WHERE order_no IN (‘a’, ‘b’, ‘c’)

image.png
这种利用索引进行范围匹配的访问方法称之为 range,此处所说的使用索引进行范围匹配中的 索引 可以是聚簇索引,也可以是二级索引。

index

当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是 index。

EXPLAIN SELECT insert_time FROM s1 WHERE expire_time = ‘2021-03-22 18:36:47’

image.png

all

最熟悉的全表扫描,将遍历全表以找到匹配的行:

EXPLAIN SELECT * FROM s1

image.png

possible_keys 与 key

在 EXPLAIN 语句输出的执行计划中,possible_keys 列表示在某个查询语句中,对某个表执行单表查询时可能用到哪些索引,key 列表示实际用到的索引有哪些,如果为 NULL 则没有使用索引。比方说下边这个查询:

EXPLAIN SELECT order_note FROM s1 WHERE insert_time = ‘2021-03-22 18:36:47’

image.png
上述执行计划的 possible keys 列的值表示该查询可能使用到 u_idx_day_status,idx_insert_time 两个索引,然后 key 列的值是 u_idx_day_status,表示经过查询优化器计算使用不同索引的成本后,最后决定使用 u_idx_day_status 来执行查询比较划算。 关于 MySQL 查询成本计算可以看我之前的文章。

不过有一点比较特别,就是在使用 index 访问方法来查询某个表时,可能会出现possible_keys 列是空的,而 key 列展示的是实际使用到的索引,比如这样:

EXPLAIN SELECT insert_time FROM s1 WHERE expire_time = ‘2021-03-22 18:36:47’

image.png
这种情况一般发生在覆盖索引条件下,possible_keys 为 null 说明用不上索引的树形查找,但如果级索引包含了所有要查找的数据,二级索引往往比聚集索引小,所以 MySQL可能会选择顺序遍历这个二级索引直接返回,所以就出现了这个情况,但是这种情况其实并没有真正用到索引。

另外需要注意的一点是,possible keys 列中的值并不是越多越好,可能使用的索引越多,查询优化器计算查询成本时就得花费更长时间,所以如果可以的话,尽量删除那些用不到的索引。

key_len

key_len 列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度。

  • 对于使用固定长度类型的索引列来说,它实际占用的存储空间最大长度就是该固定值
  • 对于指定字符集的变长类型索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是 utf8,那么该列实际占用的最大存储空间就是 100 x 3 = 300 个字节。如果该索引列可以存储 NULL 值,则 key_len 比不可以存储 NULL 值时多 1 个字节。
  • 对于变长字段来说,都会有 2 个字节的空间来存储该变长列的实际长度。

比如下面这个查询:

EXPLAIN SELECT * FROM s1 WHERE id = 718

image.png
由于 id 列的类型是 bigint,并且不可以存储 NULL 值,所以在使用该列的索引时 key_len大小就是 8。对于可变长度的索引列来说,比如下边这个查询:

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’

image.png
由于 order_no 列的类型是 VARCHAR(50),所以该列实际最多占用的存储空间就是 50*3 字节,又因为该列是可变长度列,所以 key_len 需要加 2,所以最后 ken_len 的值就是 152。

执行计划的生成是在 MySQL server 层中的功能,并不是针对具体某个存储引擎的功能,MySQL 在执行计划中输出 key_len 列主要是为了让我们区分某个使用联合索引的查询具体用了几个索引列(复合索引有最左前缀的特性,如果复合索引能全部使用上,则是复合索引字段的索引长度之和,这也可以用来判定复合索引是否部分使用,还是全部使用,而不是为了准确的说明针对某个具体存储引擎存储变长字段的实际长度占用的空间到底是占用 1 个字节还是 2 个字节。

ref

当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是 const、eg_ref、ref、
ref_or_null、unique_sutbquery、index_subopery 其中之一时,ref 列展示的就是与索引列作等值匹配的是谁,比如只是一个常数或者是某个列。比如:

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’

image.png
可以看到 ref 列的值是 const,表明在使用 idx_order_no 索引执行查询时,与 order_no 列作等值匹配的对象是一个常数,当然有时候更复杂一点:

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id

image.png
可以看到对被驱动表 s1 的访问方法是 eg_ref,而对应的 ref 列的值是 mysqladv.s2.id,这说明在对被驱动表进行访问时会用到 PRIMARY 索引,也就是聚簇索引与一个列进行等值匹配的条件,与 s2 表的 id 作等值匹配的对象就是 mysqladv.s2.id((注意这里把数据库名也写出来了)

rows

如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的 rows 列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的 rows 列就代表预计扫描的索引记录行数。比如下边这个查询:

EXPLAIN SELECT * FROM s1 WHERE order_no > ‘z’

image.png
我们看到执行计划的 rows 列的值是 1,这意味查询优化器在经过分析使 idx_order_no 进行查询的成本之后,觉得满足 order_no> ‘ a ‘这个条件的记录只有 1条。

filtered

filtered 代表查询优化器预测有多少条记录满⾜其余的搜索条件,什么意思呢?我们具体看下面查询语句(对于单表查询来说,这个 filtered 列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的 filtered 值):

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_no = s2.order_no WHERE s1.order_note > ‘你好,李焕英’

image.png
从执行计划中可以看出来,查询优化器打算把 s 1 当作驱动表,s2 当作被驱动表。我们可以看到驱动表 s1 表的执行计划的 rows 列为 10573,filtered 列为 33.33 ,这意味着驱动表 s1 的扇出值就是 10573 x 33.33 % = 3524.3,这说明还要对被驱动表执行大约 3524 次
查询。

Extra

顾名思义,Extra 列是用来说明一些额外信息的,我们可以通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句。MySQL 提供的额外信息很多,几十个,我们挑一些平时常见的或者比较重要的额外信息讲讲。

Using index

当我们的查询列表以及搜索条件中只包含属于某个索引的列(也就是在可以使用索引覆盖的情况下)在 Extra 列将会提示 Using index 额外信息。比方说下边这个查询中只需要用到idx_order_no 而不需要回表操作:

EXPLAIN SELECT expire_time FROM s1 WHERE insert_time = ‘2021-03-22 18:36:47’

image.png

Using index condition

有些搜索条件中虽然出现了索引列,但却不能使用到索引,比如下边这个查询:

SELECT * FROM s1 WHERE order_no > ‘z’ AND order_no LIKE ‘%a’

其中的 order_no> ‘z’ 可以使用到索引,但是 order_no LIKE ‘%a’ 却无法使用到索引,在以前版本的 MySQL 中,是按照下边步骤来执行这个查询的:

  1. 先根据 order_no> ‘z’ 这个条件,从二级索引 idx_order_no 中获取到对应的记录
  2. 根据上一步骤得到的二级索引记录中的主键值进行回表,找到完整的用户记录再检测该记录是否符合 key1 LIKE ‘%a’ 这个条件,将符合条件的记录加入到最后的结果集。

索引下推
虽然 order_no LIKE ‘%a’不能组成范围区间参与 range 访问方法的执行,但这个条件毕竟只涉及到了 order_no 列,MySQL 把上边的步骤改进了一下。

  1. 根据 order_no> ‘z’ 这个条件,定位到二级索引 idx_order_no 中对应的二级索引记录
  2. 对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足 order_no LIKE ‘%a’ 这个条件,如果这个条件不满足,则该二级索引记录压根儿就没必要回表
  3. 对于满足 order_no LIKE ‘%a’ 这个条件的二级索引记录执行回表操作

我们说回表操作其实是一个随机 IO,比较耗时,所以上述修改可以省去很多回表操作的成本,这个改进称之为索引条件下推。如果在查询语句的执行过程中将要使用索引条件下推这个特性,在 Extra 列中将会显示 Using index condition:
image.png

Using where

当我们使用全表扫描来执行对某个表的查询,并且该语句的 WHERE 子句中有针对该表的搜索条件时,在 Extra 列中会提示上述额外信息。

EXPLAIN SELECT * FROM s1 WHERE order_note = ‘a’

image.png
当使用索引访问来执行对某个表的查询,并且该语句的 WHERE 子句中有除了该索引包含的列之外的其他搜索条件时,在 Extra 列中也会提示上述信息。比如下边这个查询虽然使用 idx_order_no 索引执行查询:

EXPLAIN SELECT * FROM s1 WHERE order_no = ‘a’ AND order_note = ‘a’

但是搜索条件中除了包含 order_no 的搜索条件 order_no = ‘a’,还有包含 order_note 的搜索条件,此时需要回表检索记录然后进行条件判断,所以 Extra 列会显示 Using where 的提示:
image.png
注意:出现了 Using where,只是表示在 server 层根据 where 条件进行了过滤,和是否全表扫描或读取了索引文件没有关系,网上有不少文章把 Using where 和是否读取索引进行关联,是不正确的,也有文章把 Using where 和回表进行了关联,这也是不对的。

Using join buffer

在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL 一般会为其分配一块名叫 join buffer 的内存块来加快查询速度:

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_note = s2.order_note

image.png
我们看到,在对 s1 表的执行计划的 Extra 列显示了两个提示:

  • Using join buffer (Block Nested Loop):这是因为对表 s1 的访问不能有效利用索引,只好退而求其次,使用 join buffer 来减少对 s1 表的访问次数,从而提高性能。
  • Using where:可以看到查询语句中有一个 s1.order_note = s2.order_note 条件,因为 s2 是驱动表,s1 是被驱动表,所以在访问 s1 表时,s1.order_note 的值已经确定下来了,所以实际上查询 s1 表的条件就是 s1.order_note = 一个常数,所以提示了Using where 额外信息。

Not exists

当我们使用左外连接时,如果 WHERE 子句中包含要求被驱动表的某个列等于 NULL 值的搜索条件,而且那个列又是不允许存储 NULL 值的,那么在该表的执行计划的 Extra 列就会提示 Not exists 额外信息,比如这样:

EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.order_no = s2.order_no WHERE s2.id IS NULL

image.png
上述查询中 s1 表是驱动表,s2 表是被驱动表,s2.id 列是主键而且不允许存储 NULL 值的,而 WHERE 子句中又包含 s2.id IS NULL 的搜索条件。

Using filesort

有一些情况下对结果集中的记录进行排序是可以使用到索引的,比如下边这个查询:

EXPLAIN SELECT * FROM s1 ORDER BY order_no LIMIT 10

image.png
这个查询语句可以利用 idx_order_no 索引直接取出 order_no 列的 10 条记录,然后再进行回表操作就好了。

但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL 把这种在内存中或者磁盘上进行排序的方式统称为文件排序。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的 Extra 列中显示 Using filesort 提示:

EXPLAIN SELECT * FROM s1 ORDER BY order_note LIMIT 10

image.png
需要注意的是,如果查询中需要使用 filesort 的方式进行排序的记录非常多,那么这个过程是很耗费性能的,我们最好想办法将使用文件排序的执行方式改为使用索引进行排序。

Using temporary

在许多查询的执行过程中,MySQL 可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含 DISTINCT、GROUP BY、UNION 等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL 很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的 Extra 列将会显示 Using temporary 提示:

EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note

image.png
上述执行计划的 Extra 列不仅仅包含 Using temporary 提示,还包含 Using filesort 提示,可是我们的查询语句中明明没有写 ORDER BY 子句呀?这是因为 MySQL 会在包含GROUP BY 子句的查询中默认添加上 ORDER BY 子句,也就是说上述查询其实和下边这个查询等价:

EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by order_note

如果我们并不想为包含 GROUP BY 子句的查询进行排序,需要我们显式的写上 ORDER BY NULL:

EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by null

image.png
这回执行计划中就没有 Using filesort 的提示了,也就意味着执行查询时可以省去对记录进行文件排序的成本了。

很明显,执行计划中出现 Using temporary 并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们最好能使用索引来替代掉使用临时表。