EXPLAIN执行计划详解

文章目录

    • 一、EXPLAIN基础
      • [1.1 什么是EXPLAIN?](#1.1 什么是EXPLAIN?)
      • [1.2 基本用法](#1.2 基本用法)
    • 二、EXPLAIN输出字段详解
      • [2.1 标准EXPLAIN输出示例](#2.1 标准EXPLAIN输出示例)
      • [2.2 字段含义总览](#2.2 字段含义总览)
    • 三、实际案例分析
      • [3.1 案例1:主键查询(最优)](#3.1 案例1:主键查询(最优))
      • [3.2 案例2:普通索引查询](#3.2 案例2:普通索引查询)
      • [3.3 案例3:范围查询](#3.3 案例3:范围查询)
      • [3.4 案例4:全表扫描(最差)](#3.4 案例4:全表扫描(最差))
      • [3.5 案例5:覆盖索引](#3.5 案例5:覆盖索引)
      • [3.6 案例6:索引失效](#3.6 案例6:索引失效)
      • [3.7 案例7:多表JOIN](#3.7 案例7:多表JOIN)
      • [3.8 案例8:子查询](#3.8 案例8:子查询)
      • [3.9 案例9:UNION查询](#3.9 案例9:UNION查询)
    • 四、type类型详解
      • [4.1 type类型性能排序](#4.1 type类型性能排序)
      • [4.2 system - 表中只有一行](#4.2 system - 表中只有一行)
      • [4.3 const - 主键或唯一索引等值查询](#4.3 const - 主键或唯一索引等值查询)
      • [4.4 eq_ref - JOIN时使用主键或唯一索引](#4.4 eq_ref - JOIN时使用主键或唯一索引)
      • [4.5 ref - 非唯一索引等值查询](#4.5 ref - 非唯一索引等值查询)
      • [4.6 ref_or_null - ref + NULL值查询](#4.6 ref_or_null - ref + NULL值查询)
      • [4.7 index_merge - 合并多个索引](#4.7 index_merge - 合并多个索引)
      • [4.8 range - 范围扫描](#4.8 range - 范围扫描)
      • [4.9 index - 全索引扫描](#4.9 index - 全索引扫描)
      • [4.10 ALL - 全表扫描](#4.10 ALL - 全表扫描)
    • 五、Extra信息详解
      • [5.1 Using index - 覆盖索引(最优)](#5.1 Using index - 覆盖索引(最优))
      • [5.2 Using where - WHERE过滤](#5.2 Using where - WHERE过滤)
      • [5.3 Using index condition - 索引下推(ICP)](#5.3 Using index condition - 索引下推(ICP))
      • [5.4 Using filesort - 文件排序(需优化)](#5.4 Using filesort - 文件排序(需优化))
      • [5.5 Using temporary - 使用临时表(需优化)](#5.5 Using temporary - 使用临时表(需优化))
      • [5.6 Using join buffer - JOIN缓冲](#5.6 Using join buffer - JOIN缓冲)
      • [5.7 Impossible WHERE - WHERE条件永远为假](#5.7 Impossible WHERE - WHERE条件永远为假)
      • [5.8 Select tables optimized away](#5.8 Select tables optimized away)
      • [5.9 Using union/intersect/sort_union](#5.9 Using union/intersect/sort_union)
    • 六、EXPLAIN的变体
      • [6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)](#6.1 EXPLAIN ANALYZE(MySQL 8.0.18+))
      • [6.2 EXPLAIN FORMAT=JSON](#6.2 EXPLAIN FORMAT=JSON)
      • [6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)](#6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+))
    • 七、优化实战案例
      • [7.1 案例1:慢查询优化](#7.1 案例1:慢查询优化)
      • [7.2 案例2:JOIN优化](#7.2 案例2:JOIN优化)
      • [7.3 案例3:子查询优化](#7.3 案例3:子查询优化)
    • 八、常见问题诊断
      • [8.1 如何判断SQL是否需要优化?](#8.1 如何判断SQL是否需要优化?)
      • [8.2 为什么possible_keys有值,但key是NULL?](#8.2 为什么possible_keys有值,但key是NULL?)
      • [8.3 如何理解key_len?](#8.3 如何理解key_len?)
      • [8.4 rows和filtered的关系](#8.4 rows和filtered的关系)
      • [8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?](#8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?)
    • 总结

一、EXPLAIN基础

1.1 什么是EXPLAIN?

EXPLAIN 是MySQL提供的查询分析工具,用于查看SQL语句的执行计划,帮助我们了解:

  • MySQL如何执行这条SQL
  • 是否使用了索引
  • 扫描了多少行数据
  • 是否需要临时表或排序

1.2 基本用法

sql 复制代码
-- 基本语法
EXPLAIN SELECT * FROM users WHERE id = 1;

-- 查看详细信息(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;

-- JSON格式(更详细)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;

-- 传统格式(MySQL 5.7+)
EXPLAIN FORMAT=TRADITIONAL SELECT * FROM users WHERE id = 1;

-- 树形格式(MySQL 8.0.16+)
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE id = 1;

二、EXPLAIN输出字段详解

2.1 标准EXPLAIN输出示例

sql 复制代码
EXPLAIN SELECT * FROM users WHERE id = 10;

输出结果

复制代码
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | users | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

2.2 字段含义总览

字段 含义 重要性
id 查询序列号 ⭐⭐⭐
select_type 查询类型 ⭐⭐⭐
table 访问的表 ⭐⭐⭐⭐
partitions 匹配的分区 ⭐⭐
type 访问类型 ⭐⭐⭐⭐⭐ 最重要
possible_keys 可能使用的索引 ⭐⭐⭐
key 实际使用的索引 ⭐⭐⭐⭐⭐ 最重要
key_len 索引使用的字节数 ⭐⭐⭐⭐
ref 索引的哪一列被使用 ⭐⭐⭐
rows 预估扫描行数 ⭐⭐⭐⭐⭐ 最重要
filtered 过滤后的行百分比 ⭐⭐⭐
Extra 额外信息 ⭐⭐⭐⭐⭐ 最重要

三、实际案例分析

3.1 案例1:主键查询(最优)

sql 复制代码
-- 测试表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    email VARCHAR(100)
);

-- 查询
EXPLAIN SELECT * FROM users WHERE id = 10;

执行结果

复制代码
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

字段解读

  • id = 1:第一个(也是唯一的)SELECT
  • select_type = SIMPLE:简单查询(非子查询、非UNION)
  • table = users:访问users表
  • type = const最优! 通过主键或唯一索引查询单行
  • possible_keys = PRIMARY:可能使用主键索引
  • key = PRIMARY:实际使用了主键索引
  • key_len = 4:索引长度4字节(INT类型)
  • ref = const:使用常量比较
  • rows = 1:只需扫描1行
  • Extra = NULL:无额外信息

性能评价:⭐⭐⭐⭐⭐ 完美!


3.2 案例2:普通索引查询

sql 复制代码
-- 创建索引
CREATE INDEX idx_name ON users(name);

-- 查询
EXPLAIN SELECT * FROM users WHERE name = '张三';

执行结果

复制代码
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+
| id | select_type | table | type | possible_keys | key      | key_len | ref   | rows | Extra     |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+
|  1 | SIMPLE      | users | ref  | idx_name      | idx_name | 203     | const |    5 | NULL      |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+

字段解读

  • type = ref:非唯一索引等值查询
  • key = idx_name:使用了name索引
  • key_len = 203:VARCHAR(50) × 4字节(utf8mb4) + 2字节(长度) + 1字节(NULL) = 203
  • rows = 5:预计扫描5行(可能有5个叫"张三"的用户)

性能评价:⭐⭐⭐⭐ 很好!


3.3 案例3:范围查询

sql 复制代码
-- 创建索引
CREATE INDEX idx_age ON users(age);

-- 查询
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;

执行结果

复制代码
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
|  1 | SIMPLE      | users | range | idx_age       | idx_age | 5       | NULL |  500 | Using index condition |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+

字段解读

  • type = range:范围扫描
  • key = idx_age:使用了age索引
  • key_len = 5:INT(4字节) + NULL标志(1字节)
  • ref = NULL:范围查询无ref
  • rows = 500:预计扫描500行
  • Extra = Using index condition:使用了索引下推优化(ICP)

性能评价:⭐⭐⭐ 较好


3.4 案例4:全表扫描(最差)

sql 复制代码
-- 没有索引的列
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

执行结果

复制代码
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

字段解读

  • type = ALL最差! 全表扫描
  • possible_keys = NULL:没有可用索引
  • key = NULL:未使用索引
  • rows = 100000:需要扫描全部10万行
  • Extra = Using where:使用WHERE过滤,但在存储引擎层之后

性能评价:⭐ 很差!需要优化

优化方案

sql 复制代码
-- 添加索引
CREATE INDEX idx_email ON users(email);

-- 再次查询
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

优化后结果

复制代码
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key       | key_len | ref   | rows | Extra |
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+
|  1 | SIMPLE      | users | ref  | idx_email     | idx_email | 403     | const |    1 | NULL  |
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+

性能提升:从扫描10万行 → 1行,提升10万倍!


3.5 案例5:覆盖索引

sql 复制代码
-- 创建联合索引
CREATE INDEX idx_name_age ON users(name, age);

-- 查询(只查询索引中的列)
EXPLAIN SELECT name, age FROM users WHERE name = '张三';

执行结果

复制代码
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key          | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+
|  1 | SIMPLE      | users | ref  | idx_name_age  | idx_name_age | 203     | const |    5 | Using index |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+

字段解读

  • type = ref:索引查询
  • key = idx_name_age:使用联合索引
  • Extra = Using index覆盖索引! 无需回表,性能最优

对比:需要回表的查询

sql 复制代码
-- 查询索引外的列
EXPLAIN SELECT * FROM users WHERE name = '张三';

结果

复制代码
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key          | key_len | ref   | rows | Extra |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
|  1 | SIMPLE      | users | ref  | idx_name_age  | idx_name_age | 203     | const |    5 | NULL  |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
  • Extra = NULL:需要回表查询email等其他列

3.6 案例6:索引失效

sql 复制代码
-- 索引列使用函数
EXPLAIN SELECT * FROM users WHERE YEAR(create_time) = 2024;

执行结果

复制代码
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

问题:索引列使用了函数,导致索引失效,全表扫描!

优化方案

sql 复制代码
-- 改写SQL
EXPLAIN SELECT * FROM users 
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

优化后

复制代码
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
| id | select_type | table | type  | possible_keys    | key              | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
|  1 | SIMPLE      | users | range | idx_create_time  | idx_create_time  | 5       | NULL | 5000 | Using index condition |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+

3.7 案例7:多表JOIN

sql 复制代码
-- 查询用户及其订单
EXPLAIN SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;

执行结果

复制代码
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+
| id | select_type | table | type  | possible_keys    | key     | key_len | ref       | rows | Extra       |
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+
|  1 | SIMPLE      | u     | range | idx_age          | idx_age | 5       | NULL      | 5000 | Using where |
|  1 | SIMPLE      | o     | ref   | idx_user_id      | idx_user_id | 4   | u.id      |    2 | NULL        |
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+

字段解读

  • 两行结果:表示两个表的连接
  • 第1行(驱动表):
    • table = u:users表作为驱动表
    • type = range:使用age索引范围扫描
    • rows = 5000:预计扫描5000行
  • 第2行(被驱动表):
    • table = o:orders表
    • type = ref:通过user_id索引查找
    • ref = u.id:使用users表的id字段关联
    • rows = 2:每个用户平均2个订单

执行流程

  1. 先扫描users表(5000行,age>20)
  2. 对每个用户,通过索引在orders表中查找订单(每次2行)
  3. 总扫描:5000 + (5000 × 2) = 15000行

3.8 案例8:子查询

sql 复制代码
-- 标量子查询
EXPLAIN SELECT 
    u.name,
    (SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count
FROM users u
WHERE u.age > 20;

执行结果

复制代码
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+
| id | select_type        | table  | type  | possible_keys    | key          | key_len | ref   | rows | Extra       |
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+
|  1 | PRIMARY            | u      | range | idx_age          | idx_age      | 5       | NULL  | 5000 | Using where |
|  2 | DEPENDENT SUBQUERY | orders | ref   | idx_user_id      | idx_user_id  | 4       | u.id  |    2 | Using index |
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+

字段解读

  • id = 1, select_type = PRIMARY:主查询
  • id = 2, select_type = DEPENDENT SUBQUERY:依赖外部查询的子查询
  • 子查询对每个外部行执行一次(5000次)

性能问题:DEPENDENT SUBQUERY性能较差(执行多次)

优化方案:改为JOIN

sql 复制代码
EXPLAIN SELECT 
    u.name,
    COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.age > 20
GROUP BY u.id, u.name;

3.9 案例9:UNION查询

sql 复制代码
EXPLAIN 
SELECT name FROM users WHERE age < 20
UNION
SELECT name FROM users WHERE age > 60;

执行结果

复制代码
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+
| id | select_type  | table      | type  | possible_keys    | key     | key_len | ref  | rows | Extra           |
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+
|  1 | PRIMARY      | users      | range | idx_age          | idx_age | 5       | NULL | 1000 | Using where     |
|  2 | UNION        | users      | range | idx_age          | idx_age | 5       | NULL | 500  | Using where     |
| NULL | UNION RESULT | <union1,2> | ALL   | NULL             | NULL    | NULL    | NULL | NULL | Using temporary |
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+

字段解读

  • id = 1, select_type = PRIMARY:第一个SELECT
  • id = 2, select_type = UNION:UNION的第二个SELECT
  • table = <union1,2>:UNION的临时表
  • select_type = UNION RESULT:UNION的结果集
  • Extra = Using temporary:使用了临时表(用于去重)

优化建议

sql 复制代码
-- 如果不需要去重,使用UNION ALL(性能更好)
EXPLAIN 
SELECT name FROM users WHERE age < 20
UNION ALL
SELECT name FROM users WHERE age > 60;

优化后

复制代码
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys    | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
|  1 | PRIMARY     | users | range | idx_age          | idx_age | 5       | NULL | 1000 | Using where |
|  2 | UNION       | users | range | idx_age          | idx_age | 5       | NULL | 500  | Using where |
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
  • UNION RESULT
  • Using temporary
  • 性能提升明显!

四、type类型详解

type字段是EXPLAIN中最重要的字段,表示MySQL访问数据的方式。

4.1 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          - 最差(需优化)

4.2 system - 表中只有一行

sql 复制代码
-- 系统表或只有一行的表
EXPLAIN SELECT * FROM (SELECT 1) t;

结果

复制代码
+----+-------------+-------+--------+
| id | select_type | table | type   |
+----+-------------+-------+--------+
|  1 | SIMPLE      | t     | system |
+----+-------------+-------+--------+

性能:⭐⭐⭐⭐⭐ 最优


4.3 const - 主键或唯一索引等值查询

sql 复制代码
-- 通过主键查询
EXPLAIN SELECT * FROM users WHERE id = 1;

-- 通过唯一索引查询
EXPLAIN SELECT * FROM users WHERE email = 'unique@example.com';

结果

复制代码
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

特点

  • 最多返回一行数据
  • 使用主键或唯一索引
  • 性能极佳

性能:⭐⭐⭐⭐⭐ 最优


4.4 eq_ref - JOIN时使用主键或唯一索引

sql 复制代码
-- 每个users记录在orders表中最多匹配一行(user_id是主键或唯一索引)
EXPLAIN SELECT *
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

结果

复制代码
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref       | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+
|  1 | SIMPLE      | o     | ALL    | idx_user_id   | NULL    | NULL    | NULL      | 1000 | NULL  |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.user_id |    1 | NULL  |
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+

特点

  • 用于JOIN场景
  • 被驱动表通过主键或唯一索引关联
  • 每次最多匹配一行

性能:⭐⭐⭐⭐⭐ 最优


4.5 ref - 非唯一索引等值查询

sql 复制代码
-- name是普通索引(非唯一)
EXPLAIN SELECT * FROM users WHERE name = '张三';

结果

复制代码
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key      | key_len | ref   | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
|  1 | SIMPLE      | users | ref  | idx_name      | idx_name | 203     | const |    5 | NULL  |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+

特点

  • 使用非唯一索引
  • 可能返回多行
  • 性能很好

性能:⭐⭐⭐⭐ 很好


4.6 ref_or_null - ref + NULL值查询

sql 复制代码
-- 查询name='张三'或name IS NULL
EXPLAIN SELECT * FROM users WHERE name = '张三' OR name IS NULL;

结果

复制代码
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+
| id | select_type | table | type        | possible_keys | key      | key_len | ref   | rows | Extra                 |
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+
|  1 | SIMPLE      | users | ref_or_null | idx_name      | idx_name | 203     | const |    6 | Using index condition |
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+

性能:⭐⭐⭐⭐ 很好


4.7 index_merge - 合并多个索引

sql 复制代码
-- 使用OR连接不同索引列
EXPLAIN SELECT * FROM users WHERE name = '张三' OR age = 20;

结果

复制代码
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+
| id | select_type | table | type        | possible_keys     | key               | key_len | ref  | rows | Extra                                     |
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+
|  1 | SIMPLE      | users | index_merge | idx_name,idx_age  | idx_name,idx_age  | 203,5   | NULL |   10 | Using union(idx_name,idx_age); Using where |
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+

特点

  • 使用多个索引,然后合并结果
  • 常见于OR查询

性能:⭐⭐⭐ 较好(但不如单个索引)


4.8 range - 范围扫描

sql 复制代码
-- 范围查询
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
EXPLAIN SELECT * FROM users WHERE age > 20;
EXPLAIN SELECT * FROM users WHERE age IN (20, 25, 30);
EXPLAIN SELECT * FROM users WHERE name LIKE '张%';

结果

复制代码
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
|  1 | SIMPLE      | users | range | idx_age       | idx_age | 5       | NULL |  500 | Using index condition |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+

适用场景

  • ><>=<=
  • BETWEEN ... AND ...
  • IN(...)
  • LIKE 'prefix%'

性能:⭐⭐⭐ 较好


4.9 index - 全索引扫描

sql 复制代码
-- 扫描整个索引树(比全表扫描好,但仍不理想)
EXPLAIN SELECT name FROM users;

结果

复制代码
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key      | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | index | NULL          | idx_name | 203     | NULL | 100000 | Using index |
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+

特点

  • 扫描整个索引树
  • 比全表扫描快(索引文件通常更小)
  • 但仍需优化

性能:⭐⭐ 需优化


4.10 ALL - 全表扫描

sql 复制代码
-- 没有可用索引,全表扫描
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

结果

复制代码
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

特点

  • 扫描全部数据行
  • 性能最差
  • 必须优化!

性能:⭐ 最差(需优化)


五、Extra信息详解

Extra字段提供了查询的额外信息,是优化的重要参考。

5.1 Using index - 覆盖索引(最优)

sql 复制代码
CREATE INDEX idx_name_age ON users(name, age);
EXPLAIN SELECT name, age FROM users WHERE name = '张三';

结果

复制代码
Extra: Using index

含义

  • ✅ 查询的列都在索引中
  • ✅ 无需回表查询
  • ✅ 性能最优

5.2 Using where - WHERE过滤

sql 复制代码
EXPLAIN SELECT * FROM users WHERE age > 20;

结果

复制代码
Extra: Using where

含义

  • 使用WHERE条件过滤
  • 过滤发生在Server层(而非存储引擎层)
  • 正常情况,不一定需要优化

5.3 Using index condition - 索引下推(ICP)

sql 复制代码
CREATE INDEX idx_name_age ON users(name, age);
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 20;

结果

复制代码
Extra: Using index condition

含义

  • ✅ MySQL 5.6+ 的优化特性
  • ✅ 在索引遍历时就进行过滤(而非回表后过滤)
  • ✅ 减少回表次数,提升性能

对比

复制代码
无ICP:
1. 通过name索引找到所有'张%'的记录
2. 回表获取完整数据
3. 过滤age > 20

有ICP:
1. 通过name索引找到所有'张%'的记录
2. 在索引中直接过滤age > 20(减少回表)
3. 回表获取过滤后的数据

5.4 Using filesort - 文件排序(需优化)

sql 复制代码
-- age列没有索引
EXPLAIN SELECT * FROM users ORDER BY age;

结果

复制代码
Extra: Using filesort

含义

  • ❌ MySQL需要额外的排序操作
  • ❌ 无法利用索引排序
  • ❌ 性能较差,需要优化

优化方案

sql 复制代码
-- 为排序列创建索引
CREATE INDEX idx_age ON users(age);

-- 再次查询
EXPLAIN SELECT * FROM users ORDER BY age;

优化后

复制代码
Extra: Using index

5.5 Using temporary - 使用临时表(需优化)

sql 复制代码
-- GROUP BY非索引列
EXPLAIN SELECT age, COUNT(*) FROM users GROUP BY email;

结果

复制代码
Extra: Using temporary; Using filesort

含义

  • ❌ MySQL创建临时表来处理查询
  • ❌ 常见于GROUP BY、DISTINCT、UNION
  • ❌ 性能较差,需要优化

优化方案

sql 复制代码
-- 为GROUP BY列创建索引
CREATE INDEX idx_email ON users(email);

5.6 Using join buffer - JOIN缓冲

sql 复制代码
-- JOIN列没有索引
EXPLAIN SELECT *
FROM users u
INNER JOIN orders o ON u.email = o.user_email;

结果

复制代码
Extra: Using join buffer (Block Nested Loop)

含义

  • ⚠️ JOIN列没有索引
  • ⚠️ MySQL使用join_buffer缓存
  • ⚠️ 性能不佳,建议添加索引

优化方案

sql 复制代码
CREATE INDEX idx_user_email ON orders(user_email);

5.7 Impossible WHERE - WHERE条件永远为假

sql 复制代码
EXPLAIN SELECT * FROM users WHERE 1 = 0;

结果

复制代码
Extra: Impossible WHERE

含义

  • WHERE条件永远不成立
  • MySQL直接返回空结果,不执行查询

5.8 Select tables optimized away

sql 复制代码
EXPLAIN SELECT MIN(id) FROM users;

结果

复制代码
Extra: Select tables optimized away

含义

  • ✅ 优化器直接从索引中获取结果
  • ✅ 无需扫描表
  • ✅ 性能极佳

5.9 Using union/intersect/sort_union

sql 复制代码
-- index_merge时出现
EXPLAIN SELECT * FROM users WHERE name = '张三' OR age = 20;

结果

复制代码
Extra: Using union(idx_name, idx_age); Using where

含义

  • 使用多个索引
  • union:合并索引结果(OR)
  • intersect:交集(AND)
  • sort_union:先排序再合并

六、EXPLAIN的变体

6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)

sql 复制代码
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;

输出

复制代码
-> Filter: (users.age > 20)  (cost=10.25 rows=100) (actual time=0.045..0.127 rows=95 loops=1)
    -> Table scan on users  (cost=10.25 rows=100) (actual time=0.037..0.101 rows=100 loops=1)

优势

  • ✅ 显示实际执行时间(actual time)
  • ✅ 显示实际返回行数(actual rows)
  • ✅ 显示成本估算(cost)
  • ✅ 更准确的性能分析

6.2 EXPLAIN FORMAT=JSON

sql 复制代码
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1\G

输出

json 复制代码
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1.00"
    },
    "table": {
      "table_name": "users",
      "access_type": "const",
      "possible_keys": ["PRIMARY"],
      "key": "PRIMARY",
      "used_key_parts": ["id"],
      "key_length": "4",
      "ref": ["const"],
      "rows_examined_per_scan": 1,
      "rows_produced_per_join": 1,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "0.00",
        "eval_cost": "0.10",
        "prefix_cost": "0.00",
        "data_read_per_join": "1K"
      },
      "used_columns": ["id", "name", "age", "email"]
    }
  }
}

优势

  • 提供更详细的成本信息
  • 机器可读格式
  • 便于程序化分析

6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)

sql 复制代码
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE age > 20\G

输出

复制代码
-> Filter: (users.age > 20)  (cost=10.25 rows=100)
    -> Table scan on users  (cost=10.25 rows=100)

优势

  • 树形结构,更直观
  • 显示执行流程
  • 显示成本估算

七、优化实战案例

7.1 案例1:慢查询优化

问题SQL

sql 复制代码
SELECT * FROM orders 
WHERE DATE(create_time) = '2024-01-01'
ORDER BY amount DESC;

EXPLAIN分析

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE DATE(create_time) = '2024-01-01'
ORDER BY amount DESC;

结果

复制代码
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | Extra                       |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 100000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+

问题分析

  1. type = ALL:全表扫描
  2. key = NULL:未使用索引(函数导致索引失效)
  3. rows = 100000:扫描全部10万行
  4. Extra = Using filesort:需要额外排序

优化方案

sql 复制代码
-- 1. 改写SQL(去掉函数)
SELECT * FROM orders 
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'
ORDER BY amount DESC;

-- 2. 创建联合索引
CREATE INDEX idx_create_amount ON orders(create_time, amount);

优化后EXPLAIN

复制代码
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+
| id | select_type | table  | type  | possible_keys     | key               | key_len | ref  | rows | Extra                 |
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+
|  1 | SIMPLE      | orders | range | idx_create_amount | idx_create_amount | 5       | NULL | 1000 | Using index condition |
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+

优化效果

  • type = range:索引范围扫描
  • key = idx_create_amount:使用索引
  • rows = 1000:只扫描1000行(从10万降到1000)
  • ✅ 无 Using filesort:利用索引排序
  • 性能提升:100倍

7.2 案例2:JOIN优化

问题SQL

sql 复制代码
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON CONCAT(u.id, '') = o.user_id
WHERE u.age > 20;

EXPLAIN分析

复制代码
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref  | rows   | Extra                                      |
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+
|  1 | SIMPLE      | u     | ALL  | idx_age       | NULL    | NULL    | NULL | 100000 | Using where                                |
|  1 | SIMPLE      | o     | ALL  | NULL          | NULL    | NULL    | NULL | 100000 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+

问题分析:内连接(INNER JOIN)、左连接(LEFT JOIN)、右连接(RIGHT JOIN)和全连接(FULL JOIN)

  1. ❌ JOIN条件使用了函数CONCAT()
  2. ❌ 两个表都是全表扫描
  3. ❌ 使用join buffer(说明JOIN列没有索引)
  4. ❌ 扫描行数:100000 × 100000 = 100亿次比较!

优化方案

sql 复制代码
-- 去掉JOIN条件中的函数
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;

-- 确保索引存在
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_user_id ON orders(user_id);

优化后EXPLAIN

复制代码
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| id | select_type | table | type  | possible_keys    | key         | key_len | ref       | rows | Extra       |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
|  1 | SIMPLE      | u     | range | idx_age          | idx_age     | 5       | NULL      | 5000 | Using where |
|  1 | SIMPLE      | o     | ref   | idx_user_id      | idx_user_id | 4       | u.id      |    2 | NULL        |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+

优化效果

  • type = range/ref:使用索引
  • ✅ 扫描行数:5000 + (5000 × 2) = 15000行
  • 性能提升:从100亿次降到15000次,提升66万倍!

7.3 案例3:子查询优化

问题SQL

sql 复制代码
SELECT *
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);

EXPLAIN分析

复制代码
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+
| id | select_type        | table  | type | possible_keys | key         | key_len | ref   | rows   | Extra       |
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+
|  1 | PRIMARY            | users  | ALL  | NULL          | NULL        | NULL    | NULL  | 100000 | Using where |
|  2 | DEPENDENT SUBQUERY | orders | ref  | idx_user_id   | idx_user_id | 4       | func  |     10 | Using where |
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+

问题分析

  • DEPENDENT SUBQUERY:子查询对每个外部行执行一次
  • ❌ users表全表扫描
  • ❌ 子查询执行10万次

优化方案:改为JOIN

sql 复制代码
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;

优化后EXPLAIN

复制代码
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| id | select_type | table | type  | possible_keys    | key         | key_len | ref       | rows | Extra       |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
|  1 | SIMPLE      | o     | range | idx_user_id      | idx_amount  | 5       | NULL      | 1000 | Using where |
|  1 | SIMPLE      | u     | eq_ref| PRIMARY          | PRIMARY     | 4       | o.user_id |    1 | NULL        |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+

优化效果

  • ✅ 无子查询
  • ✅ 使用索引
  • 性能提升:100倍

八、常见问题诊断

8.1 如何判断SQL是否需要优化?

看这4个关键指标

  1. type字段

    • ALLindex → 需要优化
    • rangerefeq_refconst → 良好
  2. key字段

    • NULL → 未使用索引,需要优化
    • ✅ 显示索引名 → 使用了索引
  3. rows字段

    • ❌ 数值很大(如10万+)→ 需要优化
    • ✅ 数值较小 → 良好
  4. Extra字段

    • Using filesort → 需要优化排序
    • Using temporary → 需要优化(临时表)
    • Using join buffer → 需要为JOIN列添加索引
    • Using index → 覆盖索引,最优

8.2 为什么possible_keys有值,但key是NULL?

原因:优化器认为不走索引更快(如返回大部分数据)

示例

sql 复制代码
-- 假设users表有100万行,其中80万人age>18
CREATE INDEX idx_age ON users(age);

EXPLAIN SELECT * FROM users WHERE age > 18;

结果

复制代码
possible_keys: idx_age
key: NULL
type: ALL

原因:返回80%的数据,全表扫描比索引扫描更快(避免大量回表)

解决方案

  • 缩小查询范围
  • 使用覆盖索引
  • 或接受全表扫描(可能确实是最优选择)

8.3 如何理解key_len?

key_len 表示索引使用的字节数,可以判断联合索引使用了几列。

计算规则

复制代码
INT:      4字节
BIGINT:   8字节
DATETIME: 5字节(MySQL 5.6.4+)
VARCHAR(n): n × 字符集字节数 + 2(长度) + 1(NULL标志)

字符集字节数:
- latin1: 1字节
- gbk:    2字节
- utf8:   3字节
- utf8mb4: 4字节

示例

sql 复制代码
-- 联合索引
CREATE INDEX idx_name_age_email ON users(
    name VARCHAR(50),    -- utf8mb4
    age INT,
    email VARCHAR(100)   -- utf8mb4
);

-- 查询1
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- key_len = 203
-- 计算:50 × 4 + 2 + 1 = 203
-- 结论:只使用了name列

-- 查询2
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 20;
-- key_len = 208
-- 计算:203(name) + 4(INT) + 1(NULL) = 208
-- 结论:使用了name和age两列

-- 查询3
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 20 AND email = 'test@example.com';
-- key_len = 611
-- 计算:203(name) + 5(age) + 403(email: 100×4+2+1) = 611
-- 结论:使用了全部三列

8.4 rows和filtered的关系

rows :预估扫描的行数
filtered:过滤后剩余的百分比

实际返回行数 = rows × filtered / 100

示例

sql 复制代码
EXPLAIN SELECT * FROM users WHERE age > 20 AND name = '张三';

结果

复制代码
rows: 5000
filtered: 10.00

解读

  • 扫描5000行(age > 20)
  • 其中10%满足name='张三'
  • 实际返回:5000 × 10% = 500行

8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?

原因 :EXPLAIN显示的是预估值,基于统计信息。

解决方案

sql 复制代码
-- 1. 更新统计信息
ANALYZE TABLE users;

-- 2. 使用EXPLAIN ANALYZE查看实际值(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;

总结

关键指标速查表

指标 需优化
type const, system, eq_ref ref range index ALL
key 显示索引名 显示索引名 显示索引名 显示索引名 NULL
rows < 100 < 1000 < 10000 < 100000 > 100000
Extra Using index Using where Using index condition Using filesort Using temporary

EXPLAIN优化流程

复制代码
1. 执行EXPLAIN
   ↓
2. 检查type
   - ALL/index → 添加索引
   ↓
3. 检查key
   - NULL → 索引失效,检查原因
   ↓
4. 检查rows
   - 过大 → 优化WHERE条件或索引
   ↓
5. 检查Extra
   - Using filesort → 添加排序索引
   - Using temporary → 优化GROUP BY
   - Using join buffer → 为JOIN列添加索引
   ↓
6. 再次EXPLAIN验证优化效果

最佳实践

  1. 所有生产SQL都应该EXPLAIN分析
  2. 关注type、key、rows、Extra四个关键字段
  3. 定期更新统计信息(ANALYZE TABLE)
  4. 使用EXPLAIN ANALYZE查看实际执行情况(MySQL 8.0+)
  5. 建立慢查询监控,自动EXPLAIN分析

相关推荐
Pluchon2 小时前
硅基计划5.0 MySQL 叁 E-R关系图&联合/多表查询&三大连接&子查询&合并查询
开发语言·数据库·学习·mysql
Gold Steps.2 小时前
MySQL 8+ 日志管理与数据备份恢复实战指南
数据库·mysql·数据安全
不剪发的Tony老师3 小时前
Yearning:一个免费开源的SQL审核平台
数据库·sql·mysql
christine-rr4 小时前
MySQL数据库管理、DDL、DQL、DML、DCL等总结
linux·数据库·mysql
JuneXcy4 小时前
第2章 数据库系统的核心--数据模型
数据库·mysql·oracle
emma羊羊5 小时前
【业务逻辑漏洞】认证漏洞
mysql·网络安全·靶场·业务逻辑漏洞
麦麦大数据5 小时前
D025 摩托车推荐价格预测可视化系统|推荐算法|机器学习|预测算法|用户画像与数据分析
mysql·算法·机器学习·django·vue·推荐算法·价格预测
皮皮冰燃6 小时前
关系数据库-10-[mysql5和mysql8]在windows中安装为服务并共存
windows·mysql
啊森要自信6 小时前
【MySQL 数据库】MySQL用户管理
android·c语言·开发语言·数据库·mysql