Explain 执行计划详解

什么是执行计划

我们往往通过慢查询日志,可以找到慢查询的sql具体是怎么写的。我们往往可以通过表的索引,执行引擎等等去自己判断sql为什么执行缓慢的原因。但是这明显不是高效的。

我们的MySQL中为我们提供了EXPLAN关键字来协助我们分析SQL。

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

通过使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析查询语句或是表结构的性能瓶颈,总的来说通过 EXPLAIN 我们可以帮助我们分析:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用limit
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

Explain语法的使用

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

重点的就是 EXPLAIN 后面你要分析的 SQL 语句

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

Explain不会真的执行sql,只是告诉你"自己将会按照这个方式执行sql",让你做参考优化的。

执行计划详解

为了让大家先有一个感性的认识,我们把 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:—些额外的信息

table列

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

image.png

image.png

可以看见,只涉及对 s1 表的单表查询,所以 EXPLAIN 输出中只有一条记录, 其中的 table 列的值是 s1,而连接查询的执行计划中有两条记录,这两条记录的 table 列分别是 s1 和 s2。

id列

如上所述,我们连表查询的本质其实就是多个单表查询。id就代表将一条大sql拆分成多个小sql的序号。

单表select查询(同表同select)

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

EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';

image.png

连接查询(不同表同一个select)

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

EXPLAIN SELECT * FROM s1 INNER JOIN s2;

image.png

诶?这里的id为什么都是1呢?因为每一张表都对应一条记录,但每个select都对应一个id。

多表select查询

包含子查询(不同表用不同select)

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

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = 'a';

image.png

优化器将包含子查询自动改写连接子查询(优化后不同表用同一个select)

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

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE order_no = 'a');

image.png

可以看到,虽然我们的查询语句是一个子查询,但是执行计划中 s1 和 s2 表 对应的记录的 id 值全部是 1,这就表明了查询优化器将子查询转换为了连接查询(也就是id有几个,实际sql中的select语句就有几个)。

包含 UNION 子句(不同表用不同select)

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

EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;

image.png

内部临时表(负责去重)

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

包含 UNION ALL 子句(不同表用不同select)

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

EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;

image.png

总结

我们写的一条sql语句中,可能包含了多个select小语句。每一个select语句都包含了一个或多个不同的表。

  1. 以表为单位,会产生不同的行。
  2. 同一个select所包含的表id是相同的。

select_type 列(查询类别)

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

SIMPLE

单表查询

EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';

image.png

连接查询

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, 可以对比上一个例子的效果。

UNION RESULT

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

SUBQUERY

查询语句中包含了子查询,比如下面这个sql:

EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = 'a';

image.png

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);

这个查询比较复杂,大查询里包含了一个子查询,子查询里又是由 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 语句的大 致改写情况:

image.png

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 列显示的是<derived2>,表示该查询是针 对将派生表物化之后的表进行查询的。

MATERIALIZED

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

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

image.png

执行计划的第三条记录的 id 值为 2,从它的 select_type 值为 MATERIALIED 可 以看出,查询优化器是要把子查询先转换成物化表。 然后看执行计划的前两条记录的 iad 值都为 1,说明这两条记录对应的表进 行连接查询,需要注意的是第二条记录的 table 列的值是<subquery2>,说明该表 其实就是 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

partitions列

和分区表有关,一般情况下我们的查询语句的执行计划的 partitions 列的值都是 NULL

type列(索引类别)

我们前边说过执行计划的一条记录就代表着 MySQL 对某个表的执行查询时 的访问方法/访问类型,其中的 type 列就表明了这个访问方法/访问类型是个什么东西(一般用来判断走没走索引),是较为重要的一个指标。

type结果值排序

结果值从最好到最坏依次是:

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

system(非InnoDB)

MyISAM引擎

当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如 MyISAM、Memory,那么对该表的访问方法就是 system。

explain select * from test_myisam;

image.png

InnoDB引擎

当然,如果改成使用 InnoDB 存储引擎,试试看执行计划的 type 列的值是什么

image.png

只有一条数据也全表扫描。

const(唯一查询)

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

const,意思是常数级别的,代价是可以忽略不计的。

主键索引

EXPLAIN SELECT * FROM s1 WHERE id = 716;

image.png

唯一二级索引

H){DJXEZW)IM4F]6)A(KTYD.png

唯一二级索引就是UNIQUE修饰的列索引,在进行等值操作时也是const类型。由于只有一条数据,因此即使回表速度也很快。

唯一二级索引特殊的null

对于唯一二级索引来说,查询该列为 NULL 值的情况比较特殊,因为唯一二级索引列并不限制 NULL 值的数量。这和我们的UNIQUE定义是违背的。

所以上述语句可能访问到多条记录,也就是说唯一二级索引条件为 is null 不可以使用 const 访问方法来执行。

非叶子节点在内存中有缓存

我们一般非叶子节点的索引往往在内存中存在缓存。因此实际IO次数可能更少。

eq_ref(关联查询,被驱动表条件为唯一索引)

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

驱动表与被驱动表

A 表和 B 表 join 连接查询,如果通过 A 表的结果集作为循环基础数据,然后一条一条地通过该结果集中的数据作为过滤条件到 B 表中查询数据,然后合并结果。那么我们称 A 表为驱动表,B 表为被驱动表。

实例

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

image.png

ref(普通索引查询)

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

EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';

image.png

ref和All查询方式的选择

对于这个查询,我们当然可以选择全表扫描来逐一对比搜索条件是否满足要求,我们也可以先使用二级索引找到对应记录的id值,然后再回表到聚簇索引中查找完整的用户记录。

由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以 MySQL可能选择使用索引而不是全表扫描的方式来执行查询。这种搜索条件为二级索引列与常数等值比较,采用二级索引来执行查询的访问方法称为:ref。

普通索引为null的情况

普通索引列允许为null的情况下,数量必然是不受限制的,所以我们采用 key IS NULL 这种形式的搜索条件最多只能使用 ref 的访问方法,而不是 const 的访问方法。

fulltext(全文索引)

全文索引,略过。

ref_or_null(普通二级索引+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 NULL 和 order_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(主键列子查询)

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

可以看到执行计划的第二条记录的 type 值就是 unique_subquery,说明在执行子查询时会使用到 id 列的索引

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

这个语句和 unique_subquery 章节中的唯一不同是什么?就是 in 子句的查询字段由id 变成了 order_no。

18FOIM~G6A)IF376OC4AC_E.png

range(范围)

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

EXPLAIN SELECT * FROM s1 WHERE order_no IN ('a', 'b', 'c');
EXPLAIN SELECT * FROM s1 WHERE order_no > 'a' AND order_no < 'b';

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(实际索引)

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

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

上述执行计划的 possible keys 列的值表示该查询可能使用到 u_idx_day_status,idx_insert_time 两个索引,然后 key 列的值是 u_idx_day_status, 表示经过查询优化器计算使用不同索引的成本后,最后决定使用 u_idx_day_status 来执行查询比较划算

possible_keys为空而key不为空?

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

索引结构如下:

image.png

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

image.png

表中,expire_time没有独立的索引,只存在于联合索引的最右边。

那么这种情况可能发生于覆盖索引的情况下。possible_keys为null说明用不上索引的树形查找(expire_time没有独立的索引,并且也不符合联合索引的最左前缀原则)。

但是,通过遍历整个联合索引,再通过联合索引定位到expire_time的区间,我们就可以轻松获得insert_time的值。没想到联合索引还可以这么用。只能说明这是一种特殊优化吧。

key_len(表示索引最大长度)

key_len的计算方式

key_len 列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,计算方式是这样的: 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就 是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型 是 VARCHAR(100),使用的字符集是 utf8,那么该列实际占用的最大存储空间就 是 100 x 3 = 300 个字节。

如果该索引列可以存储 NULL 值,则 key_len 比不可以存储 NULL 值时多 1 个 字节。

对于可变长度(varchar)字段来说,都会有 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 个字节。

Key_len尽量控制小

Key_len 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。

key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len 是根据表定义计算而得,不是通过表内检索出的。 注意:char 和 varchar 跟字符编码也有密切的联系,比如 latin1 占用 1 个字节, gbk 占用 2 个字节,utf8 占用 3 个字节。

ref(做等值匹配时的对象类型)

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

const(常量)

EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';

可以看到 ref 列的值是 const,表明在使用 idx_order_no 索引执行查询时,与 order_no 列作等值匹配的对象是一个常数(a),当然有时候更复杂一点,我们接着看。

其他表字段

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

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

函数

有的时候与索引列进行等值匹配的对象是一个函数,比如:

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s2.order_no= UPPER(s1.order_no);

可以看到在查询计划的 ref 列⾥输出的是 func。

image.png

rows(预估符合行数)

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

EXPLAIN SELECT * FROM s1 WHERE order_no > 'z';
EXPLAIN SELECT * FROM s1 WHERE order_no > 'a';

image.png

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

filtered(索引命中率)

单表命中率

查询优化器预测索引命中率。如下:

EXPLAIN SELECT * FROM s1 WHERE id > 5890 AND order_note = 'a';

此处 filtered 列的值是 10.0,说明查询优化器预测在 5286 条记录中有 10.00%的记录满足 order_note = 'a'这个条件。

多表命中率

对于单表查询来说,这个 filtered 列的值没什么意义,我们更关注在连接查 询中驱动表对应的执行计划记录的 filtered 值,比方说下边这个查询:

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

从执行计划中可以看出来,查询优化器打算把 s1 当作驱动表,s2 当作被驱动表。

假设驱动表 s1 表的执行计划的 rows 列为 10573,filtered 列为 33.33 ,这意味着驱动表 s1 的扇出值就是 10573 x 33.33 % = 3524.3,这说明还要对被驱动表执行大约 3524 次查询。

Extra(其他信息)

几十种信息,列举部分常见的:

No tables used

当查询语句的没有 FROM 子句时将会提示该额外信息。

Impossible WHERE

查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息。

No matching min/max row

当查询列表处有 MIN 或者 MAX 聚集函数,但是并没有符合 WHERE 子句中 的搜索条件的记录时,将会提示该额外信息。

Using index(覆盖索引无需回表)

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

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

Using index condition(索引下推)

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

SELECT * FROM s1 WHERE order_no > 'z' AND order_no LIKE '%a';

MySQL5.6前执行策略

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

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

MySQL5.6后执行策略,索引下推

1、先根据 order_no> 'z'这个条件,定位到二级索引 idx_order_no 中对应的二级索引记录。

2、对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足 order_no LIKE '%a'这个条件,如果这个条件不满足,则该二级索引记录压根儿就没必要回表。

3、对于满足 order_no LIKE '%a'这个条件的二级索引记录执行回表操作。 我们说回表操作其实是一个随机 IO,比较耗时,所以上述修改可以省去很多回表操作的成本。这个改进称之为索引条件下推(英文名:ICP ,Index Condition Pushdown)。

索引下推常出现在

如果在查询语句的执行过程中将要使用索引条件下推这个特性,在 Extra 列 中将会显示 Using index condition,比如这样:

image.png

索引下推的场景

常见的索引下推还有联合索引。总之联合索引的产生条件就是:在走了某个索引需要回表前,可以通过该索引再次筛查其它条件则再次进行筛查后回表,减少操盘IO。

Using where(仅表示对查询条件进行了过滤)

全表扫描

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

EXPLAIN SELECT * FROM s1 WHERE order_note = 'a';

image.png

走了索引,但需要回表并再次过滤(多条件情况下)

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

比如下边这个查询虽然使用 idx_order_no 索引执行查询,但是搜索条件中除 了包含 order_no 的搜索条件 order_no = 'a',还有包含 order_note 的搜索条件, 此时需要回表检索记录然后进行条件判断,所以 Extra 列会显示 Using where 的 提示:

EXPLAIN SELECT * FROM s1 WHERE order_no = 'a' AND order_note = 'a';

image.png

Using where出现的原理

出现了 Using where,只是表示在 server 层根据 where 条件进行了过滤,和是否全表扫描或读取了索引文件没有关系,网上有不少文章把 Using where 和是否读取索引进行关联,是不正确的,也有文章把 Using where 和回表进行了关联,这也是不对的。

很明显,Using where 只是表示 MySQL 使用 where 子句中的条件对记录进行了过滤。

Using join buffer (Block Nested Loop)(使用关联临时缓存)

在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度, 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中的order_note后,还需要再继续依次对s2中的order_note依次进行判断。因此需要将s1的值先临时缓存起来,可以减少对s1表的访问次数。

Using where

可以看到查询语句中有一个 s1.order_note = s2.order_note 条 件,因为 s2 是驱动表,s1 是被驱动表,所以在访问 s1 表时,s1.order_note 的 值已经确定下来了,所以实际上查询 s1 表的条件就是 s1.order_note = 一个常数, 所以提示了 Using where 额外信息。

Not exists(关联查询中是否为Null不匹配)

当我们使用左(外)连接时,如果 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 intersect(...)、Using union(...)和 Using sort_union(...)(索引合并)

如果执行计划的 Extra 列出现了 Using intersect(...)提示,说明准备使用 Intersect 索引合并的方式执行查询,括号中的...表示需要进行索引合并的索引名称;如果出现了 Using union(...)提示,说明准备使用 Union 索引合并的方式执行 查询;出现了 Using sort_union(...)提示,说明准备使用 Sort-Union 索引合并的方 式执行查询。什么是索引合并,我们后面会单独讲。

Zero limit(limit 0)

当我们的 LIMIT 子句的参数为 0 时,表示压根儿不打算从表中读出任何记录, 将会提示该额外信息。

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 DISTINCT order_note FROM s1;

image.png

再比如:

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

上述执行计划的 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默认会进行排序。如果我们并不想为包含 GROUP BY 子句的查询进行排序,需要我们显式的写 上 ORDER BY NULL:

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

image.png

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

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

image.png

从 Extra 的 Using index 的提示里我们可以看出,上述查询只需要扫描 idx_order_no 索引就可以搞定了,不再需要临时表了。

总的来说,发现在执行计划里面有using filesort或者Using temporary的时候, 特别需要注意,这往往存在着很大优化的余地,最好进行改进,变为使用 Using index 会更好。

Start temporary, End temporary

有子查询时,查询优化器会优先尝试将 IN 子查询转换成 semi-join(半连接优 化技术,本质上是把子查询上拉到父查询中,与父查询的表做 join 操作),而 semi-join 又有好多种执行策略,当执行策略为 DuplicateWeedout 时,也就是通 过建立临时表来实现为外层查询中的记录进行去重操作时,驱动表查询执行计划 的 Extra 列将显示 Start temporary 提示,被驱动表查询执行计划的 Extra 列将显 示 End temporary 提示。

LooseScan

在将 In 子查询转为 semi-join 时,如果采用的是 LooseScan 执行策略,则在 驱动表执行计划的 Extra 列就是显示 LooseScan 提示。

FirstMatch(tbl_name)

在将 In 子查询转为 半连接查询(semi-join) 时,如果采用的是 FirstMatch 执行策略,则在被驱动表执行计划的 Extra 列就是显示 FirstMatch(tbl_name)提示。

  • 4
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
数据库中,执行计划是用来描述数据库查询语句的执行方式和步骤的详细计划。其中,执行计划中的"Type"字段是指每个操作的类型。下面是一些常见的执行计划类型及其含义: 1. Full Table Scan(全表扫描):对于没有索引或者无法使用索引的查询,数据库需要扫描整个表来找到符合条件的数据。 2. Index Scan(索引扫描):使用索引进行查询,数据库根据索引的键值来定位符合条件的数据。 3. Index Unique Scan(唯一索引扫描):类似于索引扫描,但是该扫描只返回符合条件的第一条数据。 4. Index Range Scan(范围索引扫描):使用索引进行范围查询,数据库根据索引的键值范围来定位符合条件的数据。 5. Nested Loop Join(嵌套循环连接):对于两个表进行连接操作时,外层表的每一行都与内层表进行比较,以确定连接条件是否满足。 6. Hash Join(哈希连接):对于两个表进行连接操作时,将其中一个表按照连接条件进行哈希分组,然后将另一个表的数据进行哈希匹配。 7. Merge Join(合并连接):对于两个已经排序的表进行连接操作时,通过比较两个表的排序列来确定连接条件是否满足。 8. Sort(排序):对查询结果进行排序操作。 9. Group By(分组):对查询结果进行分组操作,并计算每个分组的聚合值,如求和、平均值等。 10. Aggregate(聚合):对查询结果进行聚合操作,如求和、平均值等,但不涉及分组。 这些只是一些常见的执行计划类型,实际上还有其他更复杂的类型。执行计划的选择取决于查询语句的复杂度、表的大小、索引的使用情况等多个因素。通过分析执行计划,可以帮助我们优化查询语句的性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大将黄猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值