执行计划之type详解

48 篇文章 0 订阅

MySQL学习系列


官方文档路径https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types

我们前边说过执行计划的一条记录就代表着 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

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

比如

explain select * from test_myisam;
mysql> use test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> create table test_myisam (cq INT) engine = MyISAM;
Query OK, 0 rows affected (0.08 sec)

mysql> insert into test_myisam values (1);
Query OK, 1 row affected (0.08 sec)

mysql> explain select * from test_myisam;
+----+-------------+-------------+------------+--------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table       | partitions | type   | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------------+------------+--------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | test_myisam | NULL       | system | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+-------------+------------+--------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

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


mysql> ALTER TABLE test_myisam ENGINE=InnoDB;
Query OK, 1 row affected (0.17 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> explain select * from test_myisam;
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table       | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | test_myisam | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)


const

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

EXPLAIN SELECT * FROM s1 WHERE id = 716;

mysql> EXPLAIN SELECT * FROM s1 WHERE id = 716;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | s1    | NULL       | const | PRIMARY       | PRIMARY | 8       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.11 sec)

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

SELECT * FROM order_exp WHERE insert_time=’’ and order_status='' and expire_time='' ;

这个查询的执行分两步,第一步先从u_idx_day_status对应的B+树索引中根据索引列与常数的等值比较条件定位到一条二级索引记录,然后再根据该记录的id值到聚簇索引中获取到完整的用户记录。

MySQL 把这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为: const, 意思是常数级别的, 代价是可以忽略不计的。

不过这种 const 访问方法只能在主键列或者唯一二级索引列和一个常数进行等值比较时才有效, 如果主键或者唯一二级索引是由多个列构成的话, 组成索引的每一个列都是与常数进行等值比较时, 这个 const 访问方法才有效。

对于唯一二级索引来说, 查询该列为 NULL 值的情况比较特殊, 因为唯一二级索引列并不限制 NULL 值的数量, 所以上述语句可能访问到多条记录, 也就是说 is null 不可以使用 const 访问方法来执行。


eq_ref

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

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

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+-------+----------+-------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref        | rows  | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+-------+----------+-------+
|  1 | SIMPLE      | s2    | NULL       | ALL    | PRIMARY       | NULL    | NULL    | NULL       | 10621 |   100.00 | NULL  |
|  1 | SIMPLE      | s1    | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | test.s2.id |     1 |   100.00 | NULL  |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------+-------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

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


ref

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

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

mysql> EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key          | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | s1    | NULL       | ref  | idx_order_no  | idx_order_no | 152     | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

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

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

对于普通的二级索引来说, 通过索引列进行等值比较后可能匹配到多条连续的记录, 而不是像主键或者唯一二级索引那样最多只能匹配 1 条记录, 所以这种ref 访问方法比 const 要差些, 但是在二级索引等值比较时匹配的记录数较少时的
效率还是很高的(如果匹配的二级索引记录太多那么回表的成本就太大了) 。 不过需要注意下边两种情况:
二级索引列值为 NULL 的情况
不论是普通的二级索引, 还是唯一二级索引, 它们的索引列对包含 NULL 值的数量并不限制, 所以我们采用 key IS NULL 这种形式的搜索条件最多只能使用ref 的访问方法, 而不是 const 的访问方法。

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

EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23';
EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23' AND order_status = 0;

mysql> EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23';
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | order_exp | NULL       | ref  | u_idx_day_status | u_idx_day_status | 5       | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23' AND order_status = 0;
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys    | key              | key_len | ref         | rows | filtered | Extra |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | order_exp | NULL       | ref  | u_idx_day_status | u_idx_day_status | 7       | const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

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

EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23' AND order_status > -1;

mysql> EXPLAIN SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:28:23' AND order_status > -1;
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys    | key              | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | order_exp | NULL       | range | u_idx_day_status | u_idx_day_status | 7       | NULL |    1 |   100.00 | Using index condition |
+----+-------------+-----------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)


fulltext

全文索引, 跳过~。


ref_or_null

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

explain SELECT * FROM order_exp_cut WHERE order_no= 'abc' OR order_no IS NULL;
mysql> explain SELECT * FROM order_exp_cut WHERE order_no= 'abc' OR order_no IS NULL;
+----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+
| id | select_type | table         | partitions | type        | possible_keys | key          | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | order_exp_cut | NULL       | ref_or_null | idx_order_no  | idx_order_no | 153     | const |    2 |   100.00 | Using index condition |
+----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

注意, 上面的表改为了 order_exp_cut, order_exp_cut 相对于 order_exp 就是把一些列改为了允许 null, 其他的无变化。
在这里插入图片描述
这个查询相当于先分别从 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';
mysql> EXPLAIN SELECT * FROM s1 WHERE order_no = 'a' OR insert_time = '2021-03-22 18:36:47';
+----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys                                 | key                          | key_len | ref  | rows | filtered | Extra                                                  |
+----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+
|  1 | SIMPLE      | s1    | NULL       | index_merge | u_idx_day_status,idx_order_no,idx_insert_time | idx_order_no,idx_insert_time | 152,5   | NULL |   15 |   100.00 | Using union(idx_order_no,idx_insert_time); Using where |
+----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+
1 row in set, 1 warning (0.02 sec)


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';
mysql> EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 where s1.insert_time = s2.insert_time) OR order_no = 'a';
+----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+
| id | select_type        | table | partitions | type            | possible_keys                            | key     | key_len | ref  | rows  | filtered | Extra       |
+----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+
|  1 | PRIMARY            | s1    | NULL       | ALL             | idx_order_no                             | NULL    | NULL    | NULL | 10692 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | s2    | NULL       | unique_subquery | PRIMARY,u_idx_day_status,idx_insert_time | PRIMARY | 8       | func |     1 |    10.00 | Using where |
+----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+
2 rows in set, 2 warnings (0.00 sec)

可以看到执行计划的第二条记录的 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。


mysql> EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.insert_time = s2.insert_time) OR order_no = 'a';
+----+--------------------+-------+------------+----------------+-----------------------------------------------+--------------+---------+------+-------+----------+-------------+
| id | select_type        | table | partitions | type           | possible_keys                                 | key          | key_len | ref  | rows  | filtered | Extra       |
+----+--------------------+-------+------------+----------------+-----------------------------------------------+--------------+---------+------+-------+----------+-------------+
|  1 | PRIMARY            | s1    | NULL       | ALL            | idx_order_no                                  | NULL         | NULL    | NULL | 10692 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | s2    | NULL       | index_subquery | u_idx_day_status,idx_order_no,idx_insert_time | idx_order_no | 152     | func |     1 |    10.00 | Using where |
+----+--------------------+-------+------------+----------------+-----------------------------------------------+--------------+---------+------+-------+----------+-------------+
2 rows in set, 2 warnings (0.01 sec)

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';
mysql> EXPLAIN SELECT * FROM s1 WHERE order_no IN ('a', 'b', 'c');
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | s1    | NULL       | range | idx_order_no  | idx_order_no | 152     | NULL |    3 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)

mysql> EXPLAIN SELECT * FROM s1 WHERE order_no > 'a' AND order_no < 'b';
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | s1    | NULL       | range | idx_order_no  | idx_order_no | 152     | NULL |    1 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

这种利用索引进行范围匹配的访问方法称之为: range。

此处所说的使用索引进行范围匹配中的 索引 可以是聚簇索引, 也可以是二级索引

Only rows that are in a given range are retrieved, using an index to select the rows. The key column in the output row indicates which index is used. The key_len contains the longest key part that was used. The ref column is NULL for this type.

range can be used when a key column is compared to a constant using any of the =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, LIKE, or IN() operators


index

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

EXPLAIN SELECT insert_time FROM s1 WHERE expire_time = '2021-03-22 18:36:47';
mysql> EXPLAIN SELECT insert_time FROM s1 WHERE expire_time = '2021-03-22 18:36:47';
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+-------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys | key              | key_len | ref  | rows  | filtered | Extra                    |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+-------+----------+--------------------------+
|  1 | SIMPLE      | s1    | NULL       | index | NULL          | u_idx_day_status | 12      | NULL | 10692 |    10.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+-------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

all

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

EXPLAIN SELECT * FROM s1;

mysql> EXPLAIN SELECT * FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows  | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
|  1 | SIMPLE      | s1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 10692 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在数据库中,执行计划是用来描述数据库查询语句的执行方式和步骤的详细计划。其中,执行计划中的"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(聚合):对查询结果进行聚合操作,如求和、平均值等,但不涉及分组。 这些只是一些常见的执行计划类型,实际上还有其他更复杂的类型。执行计划的选择取决于查询语句的复杂度、表的大小、索引的使用情况等多个因素。通过分析执行计划,可以帮助我们优化查询语句的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lang20150928

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

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

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

打赏作者

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

抵扣说明:

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

余额充值