现在, 我们开始深入研究MySQL中EXPLAIN命令所返回的SQL执行计划。只要能够透彻理解执行计划中的每个字段含义及其背后的执行逻辑,就能轻松掌握SQL调优技巧。
1. 概述
1.1. 执行计划概述
MySQL会在执行每条SQL语句时进行成本和规则优化,最终生成一个执行计划。这个执行计划并不是神秘的黑盒,它其实就是在告诉我们:
哪个表首先被访问
使用了什么索引
如何进行数据过滤
是否需要排序、分组
数据是如何从磁盘读取、聚簇回表等
1.2. 如何查看执行计划
只需要在SQL语句前加上EXPLAIN,就可以查看该SQL的执行计划:
EXPLAIN SELECT * FROM table;
1.3. explain 输出的执行计划字段
当执行EXPLAIN时,我们会看到如下表格:
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
各个字段的解释:
id:每个SELECT语句对应一个唯一的ID。如果SQL语句比较复杂,可能会有多个id,表示多次执行。
select_type:表示查询的类型,常见的类型包括:
SIMPLE:简单查询(不包含子查询)。
PRIMARY:最外层查询。
SUBQUERY:子查询。
UNION:UNION查询。
table:查询的表名。
partitions:涉及的表分区(如果有分区表)。
type:表示访问表的方法。它的值反映了MySQL选择的连接方式或扫描方式,常见的值包括:
ALL:全表扫描(最差的选择)。
index:全索引扫描。
range:范围扫描,通常用于范围查询。
ref:通过非唯一索引进行查询。
const:常数优化,表示对表的查询结果为常量。
possible_keys:在当前查询条件下,MySQL认为可以使用的索引。
key:实际选择的索引。如果没有选择索引,值为NULL。
key_len:索引的长度(字节数)。
ref:表示查询条件中用于索引查找的列。
rows:MySQL预计要读取的行数(基于统计信息的估算)。
filtered:表明经过WHERE条件筛选后,预计剩下的数据百分比。
Extra:一些额外的信息,例如是否使用了临时表、是否需要排序等。
1.4. 例子
假设有如下SQL:
EXPLAIN SELECT * FROM orders WHERE order_id = 1001;
可能返回的执行计划如下:
+----+-------------+--------+--------+---------------+--------+---------+-------+----------+----------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+--------+---------------+--------+---------+-------+----------+----------+-------+
| 1 | SIMPLE | orders | ref | PRIMARY | PRIMARY| 4 | const | 1 | 100.00 | NULL |
+----+-------------+--------+--------+---------------+--------+---------+-------+----------+----------+-------+
type列显示的是ref,表示order_id字段使用了索引进行查找。
key列显示PRIMARY,意味着使用了主键索引。
rows列显示1,表明MySQL预估只需读取1行数据。
2. 分析不同SQL语句的执行计划
我们继续深入分析不同SQL语句的执行计划。首先,先从最简单的SQL语句开始:
2.1. 单表查询执行计划
SQL语句:
EXPLAIN SELECT * FROM t1;
执行计划:
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 3457 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+------+---------+-------+----------+----------+-------+
解析:
id = 1:此查询对应的id是1,表示这是主查询。
select_type = SIMPLE:表示这是一个简单的查询,没有子查询或联合查询。
table = t1:表示查询的表是t1。
type = ALL:ALL表示全表扫描,因为没有WHERE条件,MySQL需要扫描整个表。
rows = 3457:预估要扫描3457行数据,意味着t1表中有3457条数据。
filtered = 100.00%:由于没有WHERE过滤条件,所以所有扫描的数据都会返回。
Extra = NULL:没有额外的信息。
通过这个执行计划,可以看出这是一个简单的全表扫描查询。
2.2. 多表关联查询执行计划
SQL语句:
EXPLAIN SELECT * FROM t1 JOIN t2;
执行计划:
+----+-------------+-------+------------+------+----------------+------+---------+-----+-------+----------+-------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+----------------+------+---------+-----+-------+----------+-------------------------------------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL| 3457 | 100.00 | NULL |
| 2 | SIMPLE | t2 | NULL | ALL | NULL | NULL | NULL | NULL| 4568 | 100.00 | Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+----------------+------+---------+-----+-------+----------+-------------------------------------------+
解析:
id = 1 和 2:这里有两个id,表示两个表的访问计划。id为1的表示查询t1表,id为2的表示查询t2表。
select_type = SIMPLE:表示这两个查询都是简单查询。
table = t1 和 t2:分别表示查询的表是t1和t2。
type = ALL:两个表都是全表扫描,意味着没有索引的情况下,MySQL需要扫描整个表。
rows = 3457 和 4568:分别表示t1表有3457行,t2表有4568行。
Extra = Using join buffer (Block Nested Loop):表示MySQL使用了”块嵌套循环”的连接方式,也就是通过临时缓存对两个表的数据进行匹配。
由于这是一个JOIN查询且没有WHERE条件,查询结果会是一个笛卡尔积,t1表的每一行数据都要和t2表的每一行数据进行匹配,从而产生更多的计算量。
2.3. 小结
在单表查询中,执行计划通常是ALL类型的全表扫描,没有过滤条件时rows等于表中的总行数,filtered为100%。
在多表JOIN查询中,执行计划展示了两个表的扫描过程,且JOIN操作通常使用”块嵌套循环”的方式。两表的每一行都会被逐一比对,导致执行计划中显示ALL扫描两张表。
通过这些基础的例子,可以理解如何通过 EXPLAIN 命令分析SQL的执行过程,为后续更复杂的查询和SQL优化打下基础。
2.4. 包含子查询的SQL语句执行计划
SQL语句:
EXPLAIN SELECT * FROM t1 WHERE x1 IN (SELECT x1 FROM t2) OR x3 = 'xxxx';
执行计划:
+----+-------------+-------+------------+-------+---------------+----------+---------+------+-------+----------+-------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+-------+----------+-------------------------+
| 1 | PRIMARY | t1 | NULL | ALL | index_x3 | NULL | NULL | NULL | 3457 | 100.00 | Using where |
| 2 | SUBQUERY | t2 | NULL | index | index_x1 | index_x1 | 507 | NULL | 4687 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+-------+----------+-------------------------+
解析:
id = 1:表示主查询的执行计划。select_type为PRIMARY,表示这是主查询。该查询有一个WHERE条件(x3 = 'xxxx'),并且可以使用index_x3索引。然而,MySQL决定不使用索引而选择全表扫描(type = ALL),因为使用x3索引可能与全表扫描的成本相差无几,可能是因为该字段的'xxxx'值分布较广。
id = 2:表示子查询的执行计划。select_type为SUBQUERY,表示这是子查询。该查询对t2表使用了index_x1索引,且type = index表示是通过扫描二级索引x1来执行的。
子查询执行计划:当查询包含子查询时,执行计划会显示主查询和子查询的执行顺序。通常,子查询的执行会被单独评估,MySQL会尽可能使用索引来优化子查询。
2.5. 包含UNION的SQL语句执行计划
SQL语句:
EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2;
执行计划:
+------|--------------|----------------|------------|-------|---------------|----------|---------|------|-------|----------|----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+------|--------------|----------------|------------|-------|---------------|----------|---------|------|-------|----------|----------------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 3457 | 100.00 | NULL |
| 2 | UNION | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 4687 | 100.00 | NULL |
| NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+------|--------------|----------------|------------|-------|---------------|----------|---------|------|-------|----------|----------------------+
解析:
id = 1 和 2:表示分别查询t1和t2表的执行计划。两个查询都使用ALL,表示全表扫描,且没有使用任何索引。
id = NULL:这是UNION的结果处理阶段,select_type为UNION RESULT。此时,MySQL使用临时表来存储合并后的结果集,并且执行去重操作,Extra中显示Using temporary,表示使用临时表来去重。<union 1,2>,这就是一个临时表的表名 如果使用 UNION ALL,则不会去重,Using temporary将不会出现。
UNION执行计划:UNION语句合并两个查询的结果,默认会进行去重操作,这在执行计划中表现为一个临时表的使用(Using temporary)。如果是UNION ALL,则不进行去重,直接返回合并的结果。
3. SQL 执行计划分析
3.1. SQL 执行计划中的 id 概念
在 SQL 执行计划的输出列中,id 代表了每个 SELECT 子句的标识。每个 SELECT 子句会对应一个 id,如果有多个 SELECT 子句(比如在 UNION 操作中),它们会有相同的 id。
在 SQL 执行计划中,id 是用来标识每个查询操作的标识符,它通常用于表示查询的执行顺序。每个 SELECT 子句或者查询操作(比如在 UNION 操作中)都会被分配一个唯一的 id。如果有多个 SELECT 子句,它们通常会被分配相同的 id,但这个 id 仍然指示了它们在执行计划中的位置。
解释:
id 是一个数字,它用来区分查询中的不同步骤或者操作,尤其是在存在多个查询时。
同一个查询的不同子句:在 UNION 或 UNION ALL 的查询中,多个子查询会分配相同的 id,因为它们在执行时是并行的,或者说它们的操作属于同一个查询的不同部分。
不同查询的不同 id:对于不同的查询操作,它们会有不同的 id,以表示执行顺序和依赖关系。
举例说明:
假设我们有一个简单的 SQL 查询,其中包含了两个子查询使用了 UNION 操作:
EXPLAIN SELECT * FROM student_score WHERE class_name = '1班'
UNION
SELECT * FROM student_score WHERE class_name = '2班';
在执行计划中,可能会看到如下输出:
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
| 1 | SIMPLE | student_score | ALL | NULL | NULL| NULL | NULL | 1000 | Using where |
| 2 | SIMPLE | student_score | ALL | NULL | NULL| NULL | NULL | 500 | Using where |
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
解析:
id = 1 和 id = 2 表示查询的两个子查询(SELECT FROM student_score WHERE class_name = '1班' 和 SELECT FROM student_score WHERE class_name = '2班')。
虽然这两个查询是并行执行的,但它们在执行计划中分别显示不同的 id,这意味着它们分别代表两个独立的查询操作。
如果我们使用了 UNION ALL,查询的子查询可能会共享相同的 id,表示它们是属于同一查询的一部分,分别执行但不需要进行合并去重。
举例:UNION ALL 的执行计划
如果使用 UNION ALL 进行查询:
EXPLAIN SELECT * FROM student_score WHERE class_name = '1班'
UNION ALL
SELECT * FROM student_score WHERE class_name = '2班';
假设我们得到以下执行计划:
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
| 1 | SIMPLE | student_score | ALL | NULL | NULL| NULL | NULL | 1000 | Using where |
| 1 | SIMPLE | student_score | ALL | NULL | NULL| NULL | NULL | 500 | Using where |
+----+-------------+----------------+-------+---------------+-----+---------+------+-------+------------------+
解析:
这里,两个查询 (SELECT FROM student_score WHERE class_name = '1班' 和 SELECT FROM student_score WHERE class_name = '2班') 都有相同的 id = 1,表示它们属于同一个 UNION ALL 查询操作。
它们分别执行相同的操作,但不需要去重,MySQL 会直接将结果合并。
总结:
不同 id 的含义:代表执行计划中的不同操作或者步骤,通常用于表示查询中不同部分的执行顺序或依赖关系。
相同 id 的含义:代表多个子查询在执行过程中属于同一组操作,通常是在 UNION 或 UNION ALL 中的不同子查询,它们是并行或联合执行的。
3.2. select_type 说明
select_type 描述了查询的类型。以下是几种常见的 select_type:
SIMPLE:简单的查询,通常是单表查询或多表连接查询。
PRIMARY:外层查询的 SELECT 类型。常见于包含子查询的情况。
UNION:UNION 查询中的第二个或后续的 SELECT 语句。
UNION RESULT:UNION 操作的结果生成,通常是将多个子查询结果进行合并并去重。
SUBQUERY:在 WHERE、FROM 或 HAVING 子句中的子查询。
DEPENDENT SUBQUERY:依赖于外部查询的子查询。
DERIVED:派生表(例如,子查询的结果会被临时物化成一个表)。
3.2.1. 示例1:包含 UNION 和子查询的 SQL 执行计划
EXPLAIN SELECT * FROM t1 WHERE x1 IN (SELECT x1 FROM t2 WHERE x1 = 'xxx' UNION SELECT x1 FROM t1 WHERE x1 = 'xxx');
执行计划:
+------+---------------------+------------+------------+-------+---------------+----------+---------+-------+-------+----------+---------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+------+---------------------+------------+------------+-------+---------------+----------+---------+-------+-------+----------+---------------------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 3467 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | t2 | NULL | ref | index_x1 | index_x1 | 899 | const | 59 | 100.00 | Using where; Using index |
| 3 | DEPENDENT UNION | t1 | NULL | ref | index_x1 | index_x1 | 899 | const | 45 | 100.00 | Using where; Using index |
| NULL | UNION RESULT | <union2,3> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+------+---------------------+------------+------------+-------+---------------+----------+---------+-------+-------+----------+---------------------------+
分析:
第一条执行计划:针对外层查询,select_type 为 PRIMARY,扫描 t1 表。
第二条和第三条执行计划:分别针对子查询中的 t2 表和 t1 表,select_type 为 DEPENDENT SUBQUERY 和 DEPENDENT UNION,它们依赖外部查询,执行 IN 和 UNION 操作。
第四条执行计划:UNION RESULT 用于处理 UNION 操作的结果,进行去重,并合并子查询的结果。
3.2.2. 示例2:包含聚合操作的子查询
EXPLAIN SELECT * FROM (SELECT x1, count(*) as cnt FROM t1 GROUP BY x1) AS _t1 WHERE cnt > 10;
执行计划:
+----+-------------+---------+------------+-------+---------------+----------+---------+------+-------+----------+---------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+-------+----------+---------------------------+
| 1 | PRIMARY | NULL | NULL | ALL | NULL | NULL | NULL | NULL | 3468 | 33.33 | Using where |
| 2 | DERIVED | t1 | NULL | index | index_x1 | index_x1 | 899 | NULL | 3568 | 100.00 | Using index |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+-------+----------+---------------------------+
分析:
第二条执行计划:子查询的 select_type 为 DERIVED,表示查询结果被物化为临时表。在这里,执行的是基于 x1 字段的分组和聚合操作,使用了 index_x1 索引。
第一条执行计划:外层查询的 select_type 为 PRIMARY,执行的是基于物化临时表的筛选操作,cnt 字段大于 10 的结果。
通过这次的转换,所有关键的内容和执行计划的细节应该都已经完整展现出来了。
3.3. type 说明
SQL 执行计划中的 type 取值解析
在 SQL 执行计划中,type 字段是非常关键的,它直接决定了查询数据的方式,影响查询性能。常见的 type 取值如下:
3.3.1. const
解释:用于主键或唯一索引的等值匹配查询,代表极为快速的查询方式。一般用于查找主键或唯一索引值时,性能接近线性,因为主键值是唯一的。
适用场景:SELECT * FROM t1 WHERE id = 110;,通过主键 id 进行等值查询。
性能:极快,通常经过几次磁盘 I/O 就能定位到结果。
3.3.2. eq_ref
解释:对于关联查询,通过主键进行等值匹配。在连接查询时,针对被驱动表(右表),如果基于主键进行等值匹配,查询方式就是 eq_ref。
适用场景:SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id;,这是一个基于主键的等值匹配查询。
执行计划示例:
EXPLAIN SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id;
执行计划:
+----+-------------+---------+-------+---------------+----------+--------------------+------+-------+-------+
| id | select_type | table | type | possible_keys | key | ref | rows | Extra | |
+----+-------------+---------+-------+---------------+----------+--------------------+------+-------+-------+
| 1 | SIMPLE | t1 | ALL | PRIMARY | NULL | NULL | 3467 | | |
| 1 | SIMPLE | t2 | eq_ref| PRIMARY | PRIMARY | test_db.t1.id | 1 | | |
+----+-------------+---------+-------+---------------+----------+--------------------+------+-------+-------+
解释:
t1 表使用 ALL 类型的扫描,因为 t1 是驱动表,并且没有 WHERE 筛选条件。
t2 表使用 eq_ref 类型,表示对于 t1 表中的每条数据,会根据主键 id 在 t2 表中进行等值匹配。
3.3.3. ref
解释:基于二级索引进行等值查询。与 eq_ref 不同的是,ref 用于二级索引,而不是主键索引。查询时会根据二级索引的值进行查找,效率稍低于 eq_ref。
适用场景:基于二级索引查询某些字段的等值匹配。
3.3.4. ref_or_null
解释:当二级索引允许 NULL 值时,查询方式会是 ref_or_null。表示查询时不仅会匹配索引值,还会匹配 NULL 值。
适用场景:当查询条件中包含 NULL 值时,且字段是二级索引。
3.3.5. index_merge
解释:表示查询时通过多个索引进行数据合并。通常用于查询条件包含多个字段,且每个字段都有单独的索引时,数据库会采用多个索引进行合并。
适用场景:单表查询时,数据库通过多个索引合并数据来执行查询。
3.3.6. range
解释:基于二级索引进行范围查询。range 类型通常用于查询条件中包含 <、>、BETWEEN、IN 等范围操作符的情况。
适用场景:例如:SELECT * FROM t1 WHERE id BETWEEN 10 AND 20;,查询 id 在指定范围内的记录。
3.3.7. index
解释:表示扫描二级索引的叶子节点。与 ALL 类型不同,index 类型表示扫描的是二级索引而非全表,通常用于覆盖索引查询。
适用场景:查询只涉及索引中的字段,且没有访问表中的数据。
3.3.8. all
解释:表示全表扫描,通常用于没有索引或索引无法有效使用的查询。
适用场景:SELECT * FROM t1;,对于没有索引的表,查询会扫描整个表的所有行。
示例 SQL 和执行计划解析
3.3.9. 示例 1:主键等值查询
EXPLAIN SELECT * FROM t1 WHERE id = 110;
执行计划:
+----+-------------+-------+--------+---------------+------+-----+----------+------------------+
| id | select_type | table | type | possible_keys | key | ref | rows | filtered | Extra |
+----+-------------+-------+--------+---------------+------+-----+----------+----------+-------+
| 1 | SIMPLE | t1 | const | PRIMARY | NULL | NULL| 1 | 100.00 | NULL |
+----+-------------+-------+--------+---------------+------+-----+----------+----------+-------+
分析:
type 为 const,表示通过主键进行等值查询,性能非常快速。
3.3.10. 示例 2:表连接查询
EXPLAIN SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id;
执行计划:
+----+-------------+-------+--------+---------------+------+--------------------------+-------+----------+-------+
| id | select_type | table | type | possible_keys | key | ref | rows | filtered | Extra |
+----+-------------+-------+--------+---------------+------+--------------------------+-------+----------+-------+
| 1 | SIMPLE | t1 | ALL | PRIMARY | NULL | NULL | 3467 | 100.00 | NULL |
| 1 | SIMPLE | t2 | eq_ref | PRIMARY | PRIMARY | test_db.t1.id | 1 | 100.00 | NULL |
+----+-------------+-------+--------+---------------+------+--------------------------+-------+----------+-------+
分析:
t1 表使用 ALL 扫描,表示没有筛选条件,所以必须扫描整个表。
t2 表使用 eq_ref,表示每次从 t1 表中取出一行数据,会基于 id 主键在 t2 表中进行快速查找。
通过这些示例,能够清楚地看到执行计划中的 type 字段如何决定查询的数据访问方式。在实际的查询中,理解不同 type 的取值对于优化查询性能非常重要,掌握这些知识有助于分析查询的瓶颈并提高数据库性能。
3.4. possible_keys 说明
我们继续来讲解执行计划的一些细节,讲一下 possible_keys。
这个 possible_keys,顾名思义,其实就是在针对一个表进行查询的时候有哪些潜在可以使用的索引。
比如你有两个索引,一个是 KEY(x1, x2, x3),一个是 KEY(x1, x2, x4),此时要是在 where 条件里要根据 x1 和 x2 两个字段进行查询,那么此时明显是上述两个索引都可以使用的,那么到底要使用哪个呢?
此时就需要通过我们之前讲解的成本优化方法,去估算使用两个索引进行查询的成本,看使用哪个索引的成本更低,那么就选择用那个索引,最终选择的索引,就是执行计划里的 key 这个字段的值了。
而 key_len,其实就是当你在 key 里选择使用某个索引之后,那个索引里的最大值的长度是多少,这个就是给你一个参考,大概知道那个索引里的值最大能有多长,就这么个意思。
而执行计划里的 ref 也相对会关键一些,当你的查询方式是索引等值匹配的时候,比如 const、ref、eq_ref、ref_or_null 这些方式的时候,此时执行计划的 ref 字段告诉你的就是:你跟索引列等值匹配的是什么?是等值匹配一个常量值?还是等值匹配另外一个字段的值?
比如 SQL 语句:
EXPLAIN SELECT * FROM t1 WHERE x1 = 'xxx'
此时如果你看他的执行计划是下面这样的:
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ref | index_x1 | index_x1 | 589 | const | 468 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
大家在上面的查询计划里可以看到,针对 t1 表的查询,type 是 ref 方式的,也就是说基于普通的二级索引进行等值匹配,然后 possible_keys 只有一个,就是 index_x1,针对 x1 字段建立的一个索引,而实际使用的索引也是 index_x1,毕竟就他一个是可以用的。
然后 key_len 是 589,意思就是说 index_x1 这个索引里的 x1 字段最大值的长度也就是 589 个字节,其实这个不算是太大,不过基本可以肯定这个 x1 字段是存储字符串的,因为是一个不规律的长度。
比较关键的是 ref 字段,它的意思是说,既然你是针对某个二级索引进行等值匹配的,那么跟 index_x1 索引进行等值匹配的是什么?是一个常量或者是别的字段?这里的 ref 的值是 const,意思就是说,是使用一个常量值跟 index_x1 索引里的值进行等值匹配的。
假设你要是用了类似如下的语句:
EXPLAIN SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id;
此时执行计划里的 ref 肯定不是 const,因为你跟 t1 表的 id 字段等值匹配的是另外一个表的 id 字段,此时 ref 的值就是那个字段的名称了,执行计划如下:
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 3457 | 100.00 | NULL |
| 1 | SIMPLE | t2 | NULL | eq_ref | PRIMARY | PRIMARY | 10 | test_db.t1.id | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------+------+----------+-------+
大家看执行计划,针对 t1 表作为驱动表执行一个全表扫描,接着针对 t1 表里每条数据都会去 t2 表根据 t2 表的主键执行等值匹配,所以第二个执行计划的 type 是 eq_ref,意思就是被驱动表基于主键进行等值匹配,而且使用的索引是 PRIMARY,就是使用了 t2 表的主键。
至于 ref,意思就是说,到底是谁跟 t2 表的聚簇索引里的主键值进行等值匹配呢?是常量值吗?
不是,是 test_db 这个库下的 t1 表的 id 字段,这里跟 t2 表的主键进行等值匹配的是 t1 表的主键 id 字段,所以 ref 这里显示的清清楚楚的。
3.5. rows、filtered
简单说一下 rows 和 filtered,这个 rows 顾名思义,就是说你使用指定的查询方式,会查出来多少条数据,而 filtered 意思就是说,在查询方式查出来的这波数据里再用上其他的不在索引范围里的查询条件,又会过滤出来百分之几的数据。
比如 SQL 语句:
EXPLAIN SELECT * FROM t1 WHERE x1 > 'xxx' AND x2 = 'xxx'
他只有一个 x1 字段建了索引,x2 字段是没有索引的,此时执行计划如下:
+----+-------------+-------+------------+-------+---------------+----------+---------+-------+-------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+-------+-------+----------+------------------------------------+
| 1 | SIMPLE | t1 | NULL | range | index_x1 | index_x1 | 458 | NULL | 1987 | 13.00 | Using index condition; Using where |
+----+-------------+-------+------------+-------+---------------+----------+---------+-------+-------+----------+------------------------------------+
上面的执行计划清晰的表明了,针对 t1 表的查询方式是 range,也就是基于索引进行范围查询,用的索引是 index_x1,也就是 x1 字段的索引,然后基于 x1 > 'xxx' 这个条件通过 index_x1 索引查询出来的数据大概是 1987 条,接着会针对这 1987 条数据再基于 where 条件里的其他条件,也就是 x2 = 'xxx' 进行过滤。
这个 filtered 是 13.00,意思是估算基于 x2 = 'xxx' 条件过滤后的数据大概是 13%,也就是说最终查出来的数据大概是 1987 * 13% = 258 条左右。
3.6. Extra
执行计划中的 extra 字段 很多人可能忽视这个字段,但它其实非常关键,因为它提供了SQL执行过程中更为细节的操作信息,尤其是涉及到查询优化和性能调优时。
extra 字段包含了更多与查询相关的附加信息,它可能帮助我们更好地理解SQL执行过程中的优化细节。除了extra字段外,其他字段如 id、table、type、key 等,主要是描述如何访问每个表,使用了哪些索引以及返回了多少数据等。
不过,extra 字段提供的信息通常是对特定查询阶段更为细致的解释。它的内容可以是很多种情况,其中一些是比较常见和有用的,值得我们特别关注。
常见的 extra 信息
3.6.1. Using index:
当执行计划的extra字段显示为“Using index”时,说明查询只需要通过二级索引就能直接返回数据,而不需要回表查询。这通常意味着查询很高效,因为避免了不必要的表扫描。
例子:
EXPLAIN SELECT x1 FROM t1 WHERE x1 = 'xxx';
执行计划:
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | t1 | NULL | ref | index_x1 | index_x1 | 456 | const | 25 | 100.00 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
这个查询使用了 index_x1 索引,且通过该索引就可以直接获取 x1 字段的值,因此执行计划显示 Extra 为“Using index”。这意味着不需要回表查询数据,只依赖索引就可以完成。
3.6.2. Using index condition:
当查询中包含如LIKE、BETWEEN、>, < 等条件时,执行计划可能会显示 Using index condition,表示查询条件通过索引条件进行筛选,但此时还需要对索引返回的结果进行额外过滤。
例子:
EXPLAIN SELECT * FROM t1 WHERE x1 > 'xxx' AND x1 LIKE '%xxx';
执行计划:
+----+-------------+-------+------------+--------+---------------+----------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+----------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | t1 | NULL | range | index_x1 | index_x1 | 456 | NULL | 150 | 20.00 | Using index condition |
+----+-------------+-------+------------+--------+---------------+----------+---------+-------+------+----------+-----------------------+
在这个查询中,x1 > 'xxx' 通过索引 index_x1 查找出来数据,但是由于查询还包含了 x1 LIKE '%xxx' 的过滤条件,所以返回的数据会在 extra 中显示“Using index condition”,即使用索引查询但需要额外的筛选。
3.6.3. Using where:
这个恐怕是最最常见的了,其实这个一般是见于你直接针对一个表扫描,没用到索引,然后where里好几个条件,就会告诉你Using where,或者是你用了索引去查找,但是除了索引之外,还需要用其他的字段进行筛选,也会告诉你Using where。
比如说下面的SQL语句:
EXPLAIN SELECT * FROM t1 WHERE x2 = ‘xxx’
这里的x2是没有建立索引的,所以此时他的执行计划就是下面这样的:
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 4578 | 15.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
大家注意看,这里说了,针对t1表进行查询,用的是全表扫描方式,没有使用任何索引,然后全表扫描,扫出来的是4578条数据,这个时候大家注意看extra里显示了Using where,意思就是说,他对每条数据都用了WHERE x2 = ‘xxx’去进行筛选。
最终filtered告诉了你,过滤出来了15%的数据,大概就是说,从这个表里筛选出来了686条数据,就这个意思。
那么如果你的where条件里有一个条件是针对索引列查询的,有一个列是普通列的筛选,类似下面的SQL语句:
EXPLAIN SELECT * FROM t1 WHERE x1 = ‘xxx’ AND x2 = ‘xxx’
此时执行计划如下:
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | t1 | NULL | ref | index_x1 | index_x1 | 458 | const | 250 | 18.00 | Using where |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
这个执行计划也是非常的清晰明了,这里针对t1表去查询,先通过ref方式直接在index_x1索引里查找,是跟const代表的常量值去查找,然后查出来250条数据,接着再用Using where代表的方式,去使用AND x2 = ‘xxx’条件进行筛选,筛选后的数据比例是18%,最终所以查出来的数据大概应该是45条。
3.6.4. Using join buffer:
另外需要提到的一点是,在多表关联的时候,有的时候你的关联条件并不是索引,此时就会用一种叫做join buffer的内存技术来提升关联的性能,比如下面的SQL语句:
EXPLAIN SELECT * FROM t1 INNER JOIN t2 ON t1.x2 = t2.x2
他们的连接条件x2是没有索引的,此时一起看看他的执行计划:
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 4578 | 100.00 | NULL |
| 1 | SIMPLE | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 3472 | 1.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
这个执行计划其实也很有意思,因为要执行join,那么肯定是先得查询t1表的数据,此时是对t1表直接全表查询,查出来4578条数据,接着似乎很明确了,就是对每条数据的x2字段的值,跑到t2表里去查对应的数据,进行关联。
但是此时因为 t2 表也没法根据索引来查,也是属于全表扫描,所以每次都得对t2表全表扫描一下,根据extra提示的Using where,就是根据t1表每条数据的x2字段的值去t2表查找对应的数据了,然后此时会用join buffer技术,在内存里做一些特殊优化,减少t2表的全表扫描次数。
Using temporary 和 Using filesort:这些是比较特殊的情况,通常发生在排序操作或者需要临时表的查询中。
3.6.5. Using filesort:
表示查询需要通过文件排序来执行,通常是因为没有索引支持的排序操作,可能会影响查询性能。
通过掌握这些 extra 字段的含义,我们能更好地理解和分析SQL执行的细节,从而为后续的SQL优化打下基础。
首先大家要知道,有的时候我们在SQL语句里进行排序的时候,如果排序字段是有索引的,那么其实是直接可以从索引里按照排序顺序去查找数据的,比如这个SQL:
EXPLAIN SELECT * FROM t1 ORDER BY x1 LIMIT 10
这就是典型的一个排序后再分页的语句,执行计划如下:
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | index | NULL | index_x1 | 458 | NULL | 10 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------+
大家可以看到,这个SQL语句,他是用了index方式访问的,意思就是说直接扫描了二级索引,而且实际使用的索引也是index_x1,本质上来说,他就是在 index_x1索引里,按照顺序找你LIMIT 10要求的10条数据罢了。
但是如果我们排序的时候是没法用到索引的,此时就会基于内存或者磁盘文件来排序,大部分时候得都基于磁盘文件来排序,比如说这个SQL:
EXPLAIN SELECT * FROM t1 ORDER BY x2 LIMIT 10
x2字段是没有索引的,执行计划如下:
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 4578 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
这个SQL很明确了,他基于x2字段来排序,是没法直接根据有序的索引去找数据的,只能把所有数据写入一个临时的磁盘文件,基于排序算法在磁盘文件里按照x2字段的值完成排序,然后再按照LIMIT 10的要求取出来头10条数据。
所以大家以后要注意一下,这种把表全数据放磁盘文件排序的做法真的是相当的糟糕,性能其实会极差的。
3.6.6. Using temporary:
表示查询需要用到临时表来保存中间结果,可能会导致性能问题,特别是在处理大数据量时。
如果我们用group by、union、distinct之类的语法的时候,万一你要是没法直接利用索引来进行分组聚合,那么他会直接基于临时表来完成,也会有大量的磁盘操作,性能其实也是极低的。
比如这个SQL:
EXPLAIN SELECT x2, COUNT(*) AS amount FROM t1 GROUP BY x2
这里的x2是没有索引的,执行计划如下:
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 5788 | 100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
这个SQL里只能对全表数据放到临时表里做大量的磁盘文件操作,然后才能完成对x2字段的不同的值去分组,分组完了以后对不同x2值的分组去做聚合操作,这个过程也是相当的耗时的,性能是极低的。
4. 总结
所以最后记住,其实未来在SQL调优的时候,核心就是分析执行计划里哪些地方出现了全表扫描,或者扫描数据过大,尽可能通过合理优化索引保证执行计划每个步骤都可以基于索引执行,避免扫描过多的数据。
评论区