Skip to content
清晨的一缕阳光
返回

EXPLAIN 执行计划详解

引言

EXPLAIN 是 MySQL 最常用的性能分析工具,它可以显示查询的执行计划,帮助我们了解 MySQL 如何执行 SQL 语句。掌握 EXPLAIN 是进行 SQL 优化的必备技能。

本章将深入讲解:

EXPLAIN 基础

基本用法

EXPLAIN SELECT * FROM users WHERE id = 1;

输出示例:

+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 8       | const |    1 | NULL  |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

EXPLAIN 输出字段

字段名说明重要性
id查询的序列号⭐⭐
select_type查询类型⭐⭐⭐
table显示的表
type访问类型⭐⭐⭐⭐⭐
possible_keys可能使用的索引⭐⭐⭐
key实际使用的索引⭐⭐⭐⭐
key_len使用索引的长度⭐⭐⭐
ref显示哪个字段或常数与索引一起使用⭐⭐
rows估算需要扫描的行数⭐⭐⭐⭐
Extra额外信息⭐⭐⭐⭐⭐

字段详解

1. id(查询序列号)

表示查询中执行 SELECT 子句或操作表的顺序:

示例 1:简单查询

EXPLAIN SELECT * FROM users WHERE id = 1;
-- id: 1

示例 2:子查询

EXPLAIN SELECT * FROM users 
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);

-- 输出:
-- +----+-------------+---------+-------+
-- | id | select_type | table   | type  |
-- +----+-------------+---------+-------+
-- |  1 | PRIMARY     | users   | ALL   |
-- |  2 | SUBQUERY    | orders  | range |
-- +----+-------------+---------+-------+
-- 先执行 id=2 的子查询,再执行 id=1 的主查询

示例 3:UNION 查询

EXPLAIN SELECT * FROM users WHERE id = 1
UNION
SELECT * FROM users WHERE id = 2;

-- 输出:
-- +----+--------------+------------+-------+
-- | id | select_type  | table      | type  |
-- +----+--------------+------------+-------+
-- |  1 | PRIMARY      | users      | const |
-- |  2 | UNION        | users      | const |
-- | NULL| UNION RESULT| <union1,2>| ALL   |
-- +----+--------------+------------+-------+

2. select_type(查询类型)

说明示例
SIMPLE简单查询(不包含子查询或 UNION)普通 SELECT
PRIMARY主查询(最外层的查询)包含子查询的外层
SUBQUERY子查询(在 SELECT 或 WHERE 子句中)SELECT (SELECT …)
DERIVED派生表(FROM 子句中的子查询)FROM (SELECT …)
UNIONUNION 的第二个或之后的查询UNION 后的 SELECT
UNION RESULTUNION 的结果表UNION 结果

示例:复杂查询

EXPLAIN 
SELECT * FROM (
    SELECT user_id, SUM(amount) as total 
    FROM orders 
    GROUP BY user_id 
    HAVING total > 1000
) t1
JOIN users u ON t1.user_id = u.id;

-- 输出:
-- +----+-------------+------------+--------+
-- | id | select_type  | table      | type   |
-- +----+-------------+------------+--------+
-- |  1 | PRIMARY      | <derived2> | ALL    |
-- |  1 | PRIMARY      | u          | eq_ref |
-- |  2 | DERIVED      | orders     | ALL    |
-- +----+-------------+------------+--------+

3. type(访问类型)⭐⭐⭐⭐⭐

最重要的字段! 表示 MySQL 在表中找到所需行的方式。

从好到坏排序:

system > const > eq_ref > ref > range > index > ALL

3.1 system(最优)

表只有一行数据(系统表),这是 const 类型的特例。

CREATE TABLE t1 (i INT);
INSERT INTO t1 VALUES (1);

EXPLAIN SELECT * FROM t1 WHERE i = 1;
-- type: system

3.2 const(极优)

表中最多匹配一行数据,通常用于主键或唯一索引查询。

-- 主键查询
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type: const

-- 唯一索引查询
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- type: const

特点:

3.3 eq_ref(优秀)

对于每个索引键,表中只有一行与之匹配。通常用于主键或唯一索引的关联查询。

EXPLAIN SELECT * FROM users u
JOIN orders o ON u.id = o.user_id;

-- 输出:
-- +----+------+-------+--------+---------------+---------+---------+------+------+
-- | id | type | table | type   | key           | key_len | ref    | rows |
-- +----+------+-------+--------+---------------+---------+---------+------+------+
-- |  1 | SIMPLE | u     | ALL    | NULL          | NULL    | NULL    | 1000 |
-- |  1 | SIMPLE | o     | eq_ref | idx_user_id   | 8       | u.id   |    1 |
-- +----+------+-------+--------+---------------+---------+---------+------+------+

特点:

3.4 ref(良好)

对于索引键,表中有多行与之匹配。用于非唯一索引查询。

-- 普通索引查询
CREATE INDEX idx_status ON orders(status);
EXPLAIN SELECT * FROM orders WHERE status = 1;
-- type: ref

-- 多表 JOIN 中的非唯一索引
EXPLAIN SELECT * FROM users u
JOIN orders o ON u.id = o.user_id;
-- users 表:type: const(主键)
-- orders 表:type: ref(非唯一索引)

特点:

3.5 range(中等)

索引范围扫描,适用于范围查询。

-- 范围查询
EXPLAIN SELECT * FROM orders 
WHERE create_time >= '2024-01-01';
-- type: range

-- IN 查询
EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5);
-- type: range

-- BETWEEN 查询
EXPLAIN SELECT * FROM products 
WHERE price BETWEEN 100 AND 500;
-- type: range

特点:

3.6 index(较差)

全索引扫描,扫描整个索引树。

-- 覆盖索引查询
EXPLAIN SELECT id, name FROM users;
-- type: index(扫描整个索引树,但无需回表)

-- ORDER BY 索引列
EXPLAIN SELECT * FROM users ORDER BY create_time;
-- type: index

特点:

3.7 ALL(最差)

全表扫描,性能最差。

-- 无索引查询
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- type: ALL(name 列无索引)

-- 函数导致索引失效
EXPLAIN SELECT * FROM orders WHERE YEAR(create_time) = 2024;
-- type: ALL

特点:

4. possible_keys 和 key

EXPLAIN SELECT * FROM users 
WHERE name = '张三' AND age = 25;

-- 输出:
-- possible_keys: idx_name, idx_age, idx_name_age
-- key: idx_name_age

分析:

5. key_len(索引长度)

表示使用的索引长度(字节数),越短越好。

计算规则:

常见类型的长度:

CREATE TABLE test (
    id INT NOT NULL,           -- 4 字节
    name VARCHAR(50),          -- 可变长度
    phone CHAR(11) NOT NULL,   -- 11 字节
    create_time DATETIME       -- 5 字节
);

-- 索引长度计算
CREATE INDEX idx_id ON test(id);
-- key_len: 4

CREATE INDEX idx_phone ON test(phone);
-- key_len: 11 (CHAR) * 3 (utf8mb4) = 33

CREATE INDEX idx_name ON test(name(10));
-- key_len: 10 (前缀) * 3 (utf8mb4) + 1 (NULL 标记) = 31

用途:

-- 联合索引:idx_name_age (name, age)
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;
-- key_len: 203(name 使用了)

EXPLAIN SELECT * FROM users WHERE name = '张三';
-- key_len: 200(只使用了 name 列)

6. rows(扫描行数)

估算需要扫描的行数,越少越好。

EXPLAIN SELECT * FROM users WHERE id = 1;
-- rows: 1(最优)

EXPLAIN SELECT * FROM users WHERE status = 1;
-- rows: 5000(需要扫描 5000 行)

EXPLAIN SELECT * FROM users;
-- rows: 10000(全表扫描)

注意:

7. Extra(额外信息)⭐⭐⭐⭐⭐

包含 MySQL 解决查询的额外信息,常见值:

7.1 Using index(好)⭐⭐⭐

使用覆盖索引,无需回表。

EXPLAIN SELECT id, name FROM users WHERE name = '张三';
-- Extra: Using index

7.2 Using where(一般)

需要在 Server 层进行 WHERE 过滤。

EXPLAIN SELECT * FROM users WHERE name = '张三';
-- Extra: Using where

注意:

7.3 Using index condition(好)⭐⭐⭐

使用了索引下推优化(MySQL 5.6+)。

EXPLAIN SELECT * FROM users 
WHERE name LIKE '张%' AND age = 25;
-- Extra: Using index condition

7.4 Using filesort(注意)⚠️

无法使用索引排序,需要额外的排序操作。

EXPLAIN SELECT * FROM users ORDER BY create_time;
-- Extra: Using filesort

优化方案:

-- 为排序列创建索引
CREATE INDEX idx_create_time ON users(create_time);

-- 或者使用 LIMIT 限制数据量
SELECT * FROM users ORDER BY create_time LIMIT 10;

7.5 Using temporary(注意)⚠️

使用了临时表,常见于 GROUP BY、DISTINCT、UNION。

EXPLAIN SELECT name, COUNT(*) FROM users GROUP BY name;
-- Extra: Using temporary

优化方案:

-- 为 GROUP BY 列创建索引
CREATE INDEX idx_name ON users(name);

-- 使用索引避免临时表
SELECT name FROM users GROUP BY name;
-- Extra: Using index(无需临时表)

7.6 Using join buffer(注意)⚠️

JOIN 时使用了连接缓冲区,通常表示关联条件没有使用索引。

EXPLAIN SELECT * FROM users u
JOIN orders o ON u.name = o.customer_name;
-- Extra: Using join buffer

优化方案:

-- 为关联列创建索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_customer_name ON orders(customer_name);

7.7 NULL(好)

没有额外信息,通常表示查询很简单。

EXPLAIN SELECT * FROM users WHERE id = 1;
-- Extra: NULL(主键查询,无需额外操作)

实战案例分析

案例 1:优化慢查询

问题 SQL:

SELECT * FROM orders 
WHERE user_id = 100 
ORDER BY create_time DESC 
LIMIT 10;
-- 执行时间:2 秒

EXPLAIN 分析:

EXPLAIN SELECT * FROM orders 
WHERE user_id = 100 
ORDER BY create_time DESC 
LIMIT 10;

-- 输出:
-- +----+------+--------+------+---------------+------+---------+------+------+
-- | id | type | table  | type | possible_keys | key  | key_len| ref  | rows | Extra          |
-- +----+------+--------+------+---------------+------+---------+------+------+
-- |  1 | SIMPLE | orders | ref  | idx_user_id | idx_user_id | 8 | const | 5000 | Using filesort |
-- +----+------+--------+------+---------------+------+---------+------+------+

问题诊断:

优化方案:

-- 创建联合索引
CREATE INDEX idx_user_time ON orders(user_id, create_time DESC);

-- 验证优化效果
EXPLAIN SELECT * FROM orders 
WHERE user_id = 100 
ORDER BY create_time DESC 
LIMIT 10;

-- 新输出:
-- type: ref
-- rows: 5000
-- Extra: NULL(无 filesort)

-- 执行时间:0.01 秒(提升 200 倍)

案例 2:JOIN 优化

问题 SQL:

SELECT u.name, o.amount 
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 1
  AND o.create_time >= '2024-01-01';
-- 执行时间:5 秒

EXPLAIN 分析:

EXPLAIN SELECT u.name, o.amount 
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 1
  AND o.create_time >= '2024-01-01';

-- 输出:
-- +----+------+-------+-------+---------------+------+---------+------+------+
-- | id | type | table | type  | possible_keys | key  | key_len| ref  | rows | Extra        |
-- +----+------+-------+-------+---------------+------+---------+------+------+
-- |  1 | SIMPLE | o     | ALL   | NULL          | NULL | NULL   | NULL | 100000 | Using where |
-- |  1 | SIMPLE | u     | eq_ref| PRIMARY       | PRIMARY | 8   | o.user_id | 1 | NULL        |
-- +----+------+-------+-------+---------------+------+---------+------+------+

问题诊断:

优化方案:

-- 为 orders 表创建联合索引
CREATE INDEX idx_status_time_user ON orders(status, create_time, user_id);

-- 验证优化效果
EXPLAIN SELECT u.name, o.amount 
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 1
  AND o.create_time >= '2024-01-01';

-- 新输出:
-- type: ref
-- key: idx_status_time_user
-- rows: 1000(从 10 万降到 1000)

-- 执行时间:0.1 秒(提升 50 倍)

案例 3:子查询优化

问题 SQL:

SELECT * FROM users 
WHERE id IN (
    SELECT user_id FROM orders WHERE amount > 1000
);
-- 执行时间:3 秒

EXPLAIN 分析:

EXPLAIN SELECT * FROM users 
WHERE id IN (
    SELECT user_id FROM orders WHERE amount > 1000
);

-- 输出:
-- +----+-------------+---------+-------+
-- | id | select_type | table   | type  |
-- +----+-------------+---------+-------+
-- |  1 | PRIMARY     | users   | ALL   |
-- |  2 | SUBQUERY    | orders  | ALL   |
-- +----+-------------+---------+-------+
-- 两个表都是全表扫描!

优化方案:

-- 方案 1:改为 JOIN
EXPLAIN SELECT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;

-- 为 orders 表创建索引
CREATE INDEX idx_amount_user ON orders(amount, user_id);

-- 方案 2:使用 EXISTS
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.amount > 1000
);

-- 执行时间:0.1 秒(提升 30 倍)

注意事项

1. EXPLAIN 的局限性

2. 结合其他工具使用

-- 开启 profiling
SET profiling = 1;
SELECT * FROM users WHERE id = 1;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

-- 使用慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- 使用 performance_schema
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

3. 不要过度优化

-- 小表不需要优化
EXPLAIN SELECT * FROM small_table WHERE status = 1;
-- 如果 small_table 只有 100 行,全表扫描也很快

-- 关注影响最大的查询
-- 优先优化执行频率高、数据量大的查询

总结

EXPLAIN 诊断流程

graph TD
    A[执行 EXPLAIN] --> B{type 是什么?}
    B -->|system/const| C[最优,无需优化]
    B -->|eq_ref/ref| D[良好,检查 rows]
    B -->|range| E[中等,可接受]
    B -->|index/ALL| F[需要优化]
    
    F --> G{key 是否为 NULL?}
    G -->|是 | H[添加索引]
    G -->|否 | I[检查索引是否合适]
    
    D --> J{Extra 是否有问题?}
    J -->|Using filesort| K[优化 ORDER BY]
    J -->|Using temporary| L[优化 GROUP BY]
    J -->|Using index| M[覆盖索引,最优]

关键要点

  1. type 字段最重要:至少达到 range 级别
  2. key 字段看索引:NULL 表示未使用索引
  3. rows 字段看数量:越少越好
  4. Extra 字段看优化:Using index 是好信号
  5. 结合业务场景:不要盲目追求全 const

优化检查清单

参考资料


分享这篇文章到:

上一篇文章
Go Mutex 互斥锁底层原理
下一篇文章
索引失效场景分析