一、EXPLAIN 工具介绍
EXPLAIN 是 MySQL 中用于分析 SQL 查询性能的关键工具,它能模拟优化器执行 SQL 语句,帮助我们发现查询性能瓶颈。使用方法很简单:在 SELECT 语句前添加 EXPLAIN 关键字,MySQL 会返回执行计划信息,而不是执行该 SQL。
sql
EXPLAIN SELECT * FROM table_name WHERE condition;
重要提示:如果 FROM 子句中包含子查询,仍会执行该子查询并将结果放入临时表中。
二、EXPLAIN 的变种
1. EXPLAIN EXTENDED
在 EXPLAIN 基础上额外提供查询优化信息。执行后可通过 SHOW WARNINGS 查看优化后的查询语句,了解优化器做了哪些优化。
sql
EXPLAIN EXTENDED SELECT * FROM film WHERE id = 1;
SHOW WARNINGS;
新增字段:
filtered:百分比值,表示将要和前一个表进行连接的行数估算(rows * filtered/100)
2. EXPLAIN PARTITIONS
相比 EXPLAIN 多了一个 partitions 字段,如果查询是基于分区表,会显示查询将访问的分区。
三、EXPLAIN 输出列详解
1. id 列
- 表示 SELECT 的序列号,有几个 SELECT 就有几个 id
- id 越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行
2. select_type 列
表示查询类型:
| 类型 | 说明 |
|---|---|
| SIMPLE | 简单查询,不包含子查询和 UNION |
| PRIMARY | 复杂查询中最外层的 SELECT |
| SUBQUERY | 包含在 SELECT 中的子查询(不在 FROM 子句中) |
| DERIVED | 包含在 FROM 子句中的子查询,MySQL 会将结果存放在临时表中 |
| UNION | 在 UNION 中的第二个和随后的 SELECT |
3. table 列
表示当前 EXPLAIN 行正在访问的表。当 FROM 子句中有子查询时,table 列是 <derivedN> 格式,表示依赖 id=N 的查询。
4. type 列
表示关联类型或访问类型,决定 MySQL 如何查找表中的行。从最优到最差依次为:
system > const > eq_ref > ref > range > index > ALL
-
NULL :优化阶段分解查询,执行阶段无需访问表或索引
sqlEXPLAIN SELECT MIN(id) FROM film; -
const, system :查询某部分被优化为常量,使用主键或唯一索引
sqlEXPLAIN SELECT * FROM (SELECT * FROM film WHERE id = 1) tmp; -
eq_ref :主键或唯一索引的所有部分被连接使用,最多返回一条记录
sqlEXPLAIN SELECT * FROM film_actor LEFT JOIN film ON film_actor.film_id = film.id; -
ref :使用普通索引或唯一索引的部分前缀,可能找到多条记录
sql-- 简单查询 EXPLAIN SELECT * FROM film WHERE name = 'film1'; -- 关联查询 EXPLAIN SELECT film_id FROM film LEFT JOIN film_actor ON film.id = film_actor.film_id; -
range :范围扫描,通常出现在 IN(), BETWEEN, >, < 等操作中
sqlEXPLAIN SELECT * FROM actor WHERE id > 1; -
index :扫描全索引,一般为覆盖索引
sqlEXPLAIN SELECT * FROM film; -
ALL :全表扫描,最差的访问类型
sqlEXPLAIN SELECT * FROM actor;
5. possible_keys 列
显示查询可能使用哪些索引来查找。
6. key 列
显示 MySQL 实际采用哪个索引来优化查询。如果没有使用索引,则为 NULL。
7. key_len 列
显示 MySQL 在索引中使用的字节数,通过该值可推断使用了索引的哪些列。
计算规则:
- 字符串:char(n)和 varchar(n),n 代表字符数(UTF-8 下,数字/字母 1 字节,汉字 3 字节)
- char(n):如果存汉字,长度为 3n 字节
- varchar(n):如果存汉字,长度为 3n+2 字节(2 字节存储长度)
- 数值类型:tinyint(1)、smallint(2)、int(4)、bigint(8)
- 时间类型:date(3)、timestamp(4)、datetime(8)
- 允许 NULL 的字段:额外 1 字节
8. ref 列
显示在 key 列记录的索引中,表查找值所用到的列或常量,如 const(常量)、字段名等。
9. rows 列
MySQL 估计要读取并检测的行数(不是结果集行数)。
10. filtered 列
百分比值,rows * filtered/100 可估算出将要和前一个表进行连接的行数。
11. Extra 列
展示额外信息,常见重要值:
| 值 | 说明 |
|---|---|
| Using index | 使用覆盖索引,查询字段都可从索引中获取,不需要回表 |
| Using where | 使用 WHERE 语句处理结果,查询列未被索引覆盖 |
| Using index condition | 查询列不完全被索引覆盖,WHERE 条件中是前导列的范围 |
| Using temporary | 创建临时表处理查询,需要优化 |
| Using filesort | 外部排序,数据量大时在磁盘排序,需要优化 |
| Select tables optimized away | 使用聚合函数(如 MIN、MAX)访问存在索引的字段 |
四、索引优化最佳实践
1. 全值匹配
sql
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND age = 22;
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND age = 22 AND position = 'manager';
2. 最左前缀法则
索引多列时,必须从索引最左前列开始且不跳过索引中的列。
sql
EXPLAIN SELECT * FROM employees WHERE name = 'Bill' AND age = 31; -- 有效
EXPLAIN SELECT * FROM employees WHERE age = 30 AND position = 'dev'; -- 无效
EXPLAIN SELECT * FROM employees WHERE position = 'manager'; -- 无效
MySQL 8.0 新特性:索引跳跃扫描(Index Skip Scan)
- 在特定条件下,即使不使用最左前缀,MySQL 也能使用索引
- 限制条件:
- 查询仅依赖一张表
- 不能使用 GROUP BY 或 DISTINCT
- 查询字段必须是索引中的列
3. 不在索引列上做任何操作
sql
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei'; -- 有效
EXPLAIN SELECT * FROM employees WHERE LEFT(name, 3) = 'LiLei'; -- 无效
日期处理优化:
sql
-- 无效
EXPLAIN SELECT * FROM employees WHERE DATE(hire_time) = '2018-09-30';
-- 有效
EXPLAIN SELECT * FROM employees WHERE hire_time >= '2018-09-30 00:00:00' AND hire_time <= '2018-09-30 23:59:59';
4. 存储引擎不能使用范围条件右边的列
sql
-- 有效
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND age = 22 AND position = 'manager';
-- 无效(age为范围条件,position无法使用索引)
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND age > 22 AND position = 'manager';
5. 尽量使用覆盖索引
sql
-- 有效(使用覆盖索引)
EXPLAIN SELECT name, age FROM employees WHERE name = 'LiLei' AND age = 23 AND position = 'manager';
-- 无效(需要回表获取其他字段)
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND age = 23 AND position = 'manager';
6. 避免使用不等于(!=, <>)和 NOT IN
sql
EXPLAIN SELECT * FROM employees WHERE name != 'LiLei'; -- 无法使用索引
7. IS NULL 和 IS NOT NULL 通常无法使用索引
sql
EXPLAIN SELECT * FROM employees WHERE name IS NULL; -- 通常无法使用索引
8. LIKE 查询优化
LIKE 'KK%':相当于等值查询,可以使用索引LIKE '%KK'和LIKE '%KK%':相当于范围查询,无法使用索引
解决方案:
sql
-- 使用覆盖索引
EXPLAIN SELECT name, age, position FROM employees WHERE name LIKE '%Lei%';
-- 无法使用覆盖索引时,考虑使用搜索引擎
9. 字符串必须加单引号
sql
EXPLAIN SELECT * FROM employees WHERE name = '1000'; -- 有效
EXPLAIN SELECT * FROM employees WHERE name = 1000; -- 无效(字符串未加单引号)
10. 少用 OR 或 IN
sql
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' OR name = 'HanMeimei'; -- 可能不使用索引
11. 范围查询优化
sql
-- 无效(MySQL优化器可能选择不走索引)
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 2000;
-- 有效(拆分范围)
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 1000;
EXPLAIN SELECT * FROM employees WHERE age >= 1001 AND age <= 2000;
五、MySQL 5.7 与 8.0 的差异
-
EXPLAIN EXTENDED:
- MySQL 5.7:需要使用
EXPLAIN EXTENDED和SHOW WARNINGS - MySQL 8.0:已废除
EXPLAIN EXTENDED,只需使用EXPLAIN即可
- MySQL 5.7:需要使用
-
索引跳跃扫描:
- MySQL 8.0 引入了索引跳跃扫描,使某些不遵循最左前缀的查询也能使用索引
- MySQL 5.7 不支持索引跳跃扫描
-
SQL 模式:
- MySQL 8.0 默认开启
ONLY_FULL_GROUP_BY,可能需要调整
sql-- 关闭ONLY_FULL_GROUP_BY SET sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY','')); - MySQL 8.0 默认开启
六、总结
- EXPLAIN 是优化 SQL 的利器,通过分析执行计划可以发现性能瓶颈。
- 索引设计需遵循最左前缀原则,但 MySQL 8.0 引入了索引跳跃扫描,提供了一些灵活性。
- 避免在索引列上做任何操作(计算、函数、类型转换),否则索引失效。
- 使用覆盖索引可以减少回表操作,提高查询效率。
- 范围查询需要谨慎处理,避免使用大范围查询导致索引失效。
- MySQL 8.0 在索引优化方面有显著改进,但设计索引时仍应遵循最佳实践。
记住:索引不是越多越好,而是越"准"越好。合理设计索引,可以大幅提升数据库查询性能,为应用带来更好的用户体验。