前言 :在数据库开发和优化过程中,SQL查询的性能直接影响应用的响应速度和用户体验。一个看似简单的查询,如果执行计划不当,可能会导致全表扫描、临时表创建、文件排序等性能问题。MySQL提供了EXPLAIN命令,让我们能够查看SQL查询的执行计划,了解数据库如何执行查询,从而识别性能瓶颈并进行优化。
本文将深入讲解:
- EXPLAIN命令的作用和基本用法
- EXPLAIN输出字段的详细含义
- 如何通过EXPLAIN分析SQL性能瓶颈
- SQL查询性能优化的实战技巧
一、EXPLAIN命令概述
1.1 什么是执行计划?
SQL执行计划展示了数据库如何执行查询的详细过程,包括:
- 使用了哪些索引
- 访问表的顺序
- MySQL做了哪些优化
- 预计扫描的行数
- 是否使用临时表或外部排序
通过分析执行计划,我们可以:
- 识别全表扫描
- 发现未使用的索引
- 定位性能瓶颈
- 优化查询结构
1.2 EXPLAIN关键字的使用指南
EXPLAIN关键字 用于显示MySQL查询的执行计划,可以帮助我们分析查询语句的效率。执行后会返回一个表格,表格的字段包括:id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra等。每个字段代表查询执行的不同方面,理解这些字段有助于我们优化查询。
1.3 如何使用EXPLAIN?
基本语法:
sql
-- 在SELECT语句前加上EXPLAIN关键字
EXPLAIN SELECT * FROM employees WHERE age > 30;
-- 也可以使用DESCRIBE或DESC(EXPLAIN的别名)
DESCRIBE SELECT * FROM employees WHERE age > 30;
DESC SELECT * FROM employees WHERE age > 30;
示例:
sql
-- 创建测试表
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
department_id INT,
INDEX idx_age (age),
INDEX idx_department (department_id)
);
-- 创建部门表
CREATE TABLE departments (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
);
-- 插入测试数据
INSERT INTO employees (name, age, department_id) VALUES
('张三', 25, 1),
('李四', 30, 2),
('王五', 28, 1),
('赵六', 35, 3);
INSERT INTO departments (id, name) VALUES
(1, '技术部'),
(2, '市场部'),
(3, '人事部');
-- 分析查询
EXPLAIN SELECT * FROM employees WHERE age > 30;
输出示例:

二、EXPLAIN输出字段详解
2.1 字段概览
| 字段 | 说明 |
|---|---|
| id | 查询的标识符,表示SELECT的序列号 |
| select_type | 查询类型(SIMPLE、PRIMARY、UNION等) |
| table | 涉及的表名 |
| partitions | 匹配的分区(如果表是分区表) |
| type | 连接类型,展示数据库如何访问数据(ALL、index、range等) |
| possible_keys | 可能使用的索引 |
| key | 实际使用的索引 |
| key_len | 使用的索引长度 |
| ref | 索引查找的参考列 |
| rows | 预计扫描的行数 |
| filtered | 过滤后的行百分比 |
| Extra | 额外的执行信息(如是否使用临时表、排序等) |
2.2 id字段 - 查询标识符
含义: 代表查询中执行SELECT子句或操作表的顺序。
规则:
- 相同的id:表示这些操作在查询中是并行的,按照id的顺序从上至下执行
- 不同的id:通常表示子查询,id值越大,优先级越高,越早执行
- id为NULL:表示这是结果集,不需要用它来查询
示例:
sql
-- 简单查询
EXPLAIN SELECT * FROM employees WHERE id = 1;
-- id: 1
-- 子查询
EXPLAIN SELECT * FROM employees WHERE department_id IN (
SELECT id FROM departments WHERE name = 'IT'
);
-- id: 1 (外查询)
-- id: 2 (子查询,先执行)
-- 联合查询
EXPLAIN SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id;
-- id: 1, id: 1 (相同,从上往下执行)
2.3 select_type字段 - 查询类型
含义: 表示查询的类型,帮助你理解查询的结构。
常见类型:
| 类型 | 说明 |
|---|---|
| SIMPLE | 简单查询,不包含子查询或UNION |
| PRIMARY | 主查询,包含子查询的外层查询 |
| SUBQUERY | 子查询中的第一个SELECT |
| DERIVED | 派生表,FROM子句中的子查询 |
| UNION | UNION中的第二个或后面的查询 |
| UNION RESULT | UNION的结果 |
示例:
sql
-- SIMPLE类型
EXPLAIN SELECT * FROM employees WHERE id = 1;
-- select_type: SIMPLE
-- SUBQUERY类型
EXPLAIN SELECT * FROM employees WHERE department_id = (
SELECT id FROM departments WHERE name = 'IT'
);
-- select_type: PRIMARY (外查询)
-- select_type: SUBQUERY (子查询)
-- DERIVED类型
EXPLAIN SELECT * FROM (SELECT * FROM employees WHERE age > 30) AS temp;
-- select_type: DERIVED
-- UNION类型
EXPLAIN SELECT name FROM employees WHERE age < 25
UNION
SELECT name FROM employees WHERE age > 50;
-- select_type: PRIMARY
-- select_type: UNION
-- select_type: UNION RESULT
2.4 table字段 - 访问的表
含义: 显示查询中正在访问的表。对于子查询,可能会显示派生表或临时表。
示例:
sql
EXPLAIN SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id;
-- table: e (employees)
-- table: d (departments)
2.5 partitions字段 - 分区信息
含义: 如果表存在分区,这一列会显示查询涉及的分区。没有分区的表,通常为NULL。
2.6 type字段 - 访问类型(最重要)
含义: 访问类型,表示MySQL查找所需行的方式。不同的访问类型性能差异很大,这是判断查询性能的关键指标。
类型从好到坏排序:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
详细说明:
1. system(最好)
- 表只有一行记录(类似于系统表)
- 这是const类型的特例
sql
EXPLAIN SELECT * FROM (SELECT 1 AS id) AS t WHERE id = 1;
-- type: system
2. const
- 通过主键或唯一索引一次性找到数据
- 查询速度非常快
sql
EXPLAIN SELECT * FROM employees WHERE id = 1;
-- type: const (如果id是主键)
3. eq_ref
- 唯一性索引扫描,每个索引键对应唯一记录
- 通常用于主键或唯一索引
sql
EXPLAIN SELECT e.*, d.*
FROM employees e
JOIN departments d ON e.department_id = d.id;
-- type: eq_ref (如果d.id是主键)
4. ref
- 非唯一性索引扫描,匹配某个值的所有行
- 使用非唯一索引或普通索引
sql
EXPLAIN SELECT * FROM employees WHERE department_id = 1;
-- type: ref (如果department_id有索引但不是唯一的)
5. range
- 检索给定范围的行,通常使用索引来选择范围
- 如BETWEEN、IN、>、<
sql
EXPLAIN SELECT * FROM employees WHERE age BETWEEN 25 AND 35;
EXPLAIN SELECT * FROM employees WHERE age > 30;
EXPLAIN SELECT * FROM employees WHERE age IN (25, 30, 35);
-- type: range
6. index
- 全索引扫描,遍历整个索引来查找匹配的行
- 比ALL好,因为索引通常比表数据小
sql
EXPLAIN SELECT department_id FROM employees;
-- type: index (只查询索引列)
7. ALL(最差)
- 全表扫描,效率最低
- 需要优化!
sql
EXPLAIN SELECT * FROM employees WHERE name LIKE '%张%';
-- type: ALL (索引失效,全表扫描)
优化建议:
- 尽量避免type为ALL的查询
- 优先使用range、ref、eq_ref等类型
- 如果出现ALL,考虑添加索引或优化查询条件
2.7 possible_keys字段 - 可能使用的索引
含义: 显示可能会用到的索引。如果为空,表示没有可用的索引。
2.8 key字段 - 实际使用的索引
含义: 显示查询实际使用的索引。如果为NULL,表示查询没有使用索引。
2.9 key_len字段 - 索引长度
含义: 表示实际使用的索引的字节长度。较小的key_len值通常表示更高效的查询。
key_len计算规则:
- INT: 4字节
- BIGINT: 8字节
- VARCHAR(n): 3n + 2字节(UTF-8编码)
- DATE: 3字节
- DATETIME: 8字节
- 允许NULL: +1字节
示例:
sql
-- 创建联合索引
CREATE INDEX idx_name_age ON employees(name, age);
-- 查询1:使用联合索引的第一列
EXPLAIN SELECT * FROM employees WHERE name = '张三';
-- key: idx_name_age
-- key_len: 303 (VARCHAR(100) * 3 + 2 + 1)
-- 查询2:使用联合索引的两列
EXPLAIN SELECT * FROM employees WHERE name = '张三' AND age = 30;
-- key: idx_name_age
-- key_len: 308 (303 + 4 + 1)
-- 查询3:只使用第二列(索引失效)
EXPLAIN SELECT * FROM employees WHERE age = 30;
-- key: NULL (索引失效)
2.10 ref字段 - 索引参考列
含义: 显示哪些列或常数用于查找索引列上的值。如果可能的话,可能会是常量。
2.11 rows字段 - 预计扫描行数
含义: MySQL估计查询所需扫描的行数。越小越好,因为它代表查询访问的数据量。
注意:
- 这是一个估计值,不是精确值
- rows越小,查询性能越好
- 结合filtered字段判断实际返回的行数
示例:
sql
-- 好的查询
EXPLAIN SELECT * FROM employees WHERE id = 1;
-- rows: 1
-- 需要优化的查询
EXPLAIN SELECT * FROM employees WHERE name LIKE '%张%';
-- rows: 10000 (全表扫描)
2.12 filtered字段 - 过滤百分比
含义: 显示返回的行数占扫描行数的百分比。值越高,表示查询条件更加精准,返回的行数越少。
2.13 Extra字段 - 额外执行信息
含义: 包含无法显示在其他列中的额外信息。
常见信息:
| Extra信息 | 说明 | 性能影响 |
|---|---|---|
| Using where | 表示查询使用了WHERE过滤条件 | 中等 |
| Using index | 表示使用了覆盖索引,查询结果只通过索引获得,避免了访问数据表 | 好 |
| Using temporary | 表示查询使用了临时表,通常在涉及排序或分组操作时出现 | 差 |
| Using filesort | 表示MySQL使用外部排序来处理结果集,而不是利用索引顺序排序 | 差 |
| Using index condition | 使用索引条件下推 | 好 |
| Using join buffer | 表示查询使用了连接缓存。若连接表的行数较多,可能需要增加join_buffer_size的大小 | 中等 |
| Impossible WHERE | 表示WHERE子句的条件总是返回false,查询无法获取任何数据 |
1. Using index(覆盖索引)
- 查询只需要索引中的数据,不需要回表
- 性能最好
sql
EXPLAIN SELECT name, age FROM employees WHERE age > 30;
-- Extra: Using index (如果name和age都在索引中)
2. Using temporary(使用临时表)
- MySQL需要创建临时表来处理查询
- 常见于GROUP BY、ORDER BY、DISTINCT等操作
- 需要优化!
sql
-- 示例:需要临时表
EXPLAIN SELECT department_id, COUNT(*)
FROM employees
GROUP BY department_id;
-- Extra: Using temporary
-- 优化:添加索引
CREATE INDEX idx_dept ON employees(department_id);
EXPLAIN SELECT department_id, COUNT(*)
FROM employees
GROUP BY department_id;
-- Extra: Using index (优化成功)
3. Using filesort(外部排序)
- MySQL需要额外的排序操作
- 常见于ORDER BY操作
- 需要优化!
sql
-- 示例:需要外部排序
EXPLAIN SELECT * FROM employees ORDER BY name;
-- Extra: Using filesort
-- 优化:添加索引
CREATE INDEX idx_name ON employees(name);
EXPLAIN SELECT * FROM employees ORDER BY name;
-- Extra: (无Using filesort,优化成功)
4. Using where
- 使用WHERE子句过滤数据
- 正常情况,但如果rows很大,需要优化
sql
EXPLAIN SELECT * FROM employees WHERE age > 30;
-- Extra: Using where
5. Impossible WHERE
- WHERE子句的条件总是返回false
sql
EXPLAIN SELECT * FROM employees WHERE 1 = 0;
-- Extra: Impossible WHERE
三、通过EXPLAIN分析SQL性能瓶颈
3.1 识别全表扫描
问题: type为ALL表示全表扫描
示例:
sql
-- 问题查询
EXPLAIN SELECT * FROM employees WHERE name LIKE '%张%';
-- type: ALL
-- rows: 10000
-- Extra: Using where
-- 优化1:使用右模糊匹配
EXPLAIN SELECT * FROM employees WHERE name LIKE '张%';
-- type: range
-- rows: 500
-- key: idx_name
-- 优化2:使用全文索引
ALTER TABLE employees ADD FULLTEXT INDEX ft_name (name);
EXPLAIN SELECT * FROM employees WHERE MATCH(name) AGAINST('张' IN NATURAL LANGUAGE MODE);
-- type: fulltext
-- rows: 100
优化策略:
- 添加合适的索引
- 修改查询条件(如右模糊匹配)
- 使用覆盖索引
- 考虑使用全文索引或搜索引擎
3.2 检查索引使用情况
问题: key为NULL表示没有使用索引
示例:
sql
-- 问题查询
EXPLAIN SELECT * FROM employees WHERE age > 30;
-- key: NULL (未使用索引)
-- 检查可能的索引
SHOW INDEX FROM employees;
-- 添加索引
CREATE INDEX idx_age ON employees(age);
-- 再次分析
EXPLAIN SELECT * FROM employees WHERE age > 30;
-- key: idx_age (使用索引)
-- type: range
-- rows: 5000
优化策略:
- 使用SHOW INDEX查看现有索引
- 根据查询条件添加合适的索引
- 定期清理无用索引
- 使用索引提示(USE INDEX、FORCE INDEX)
3.3 避免临时表和外部排序
问题: Extra中出现Using temporary或Using filesort
示例:
sql
-- 问题查询:GROUP BY
EXPLAIN SELECT department_id, COUNT(*)
FROM employees
GROUP BY department_id;
-- Extra: Using temporary; Using filesort
-- 优化:添加索引
CREATE INDEX idx_dept ON employees(department_id);
EXPLAIN SELECT department_id, COUNT(*)
FROM employees
GROUP BY department_id;
-- Extra: Using index (优化成功)
-- 问题查询:ORDER BY
EXPLAIN SELECT * FROM employees ORDER BY name, age;
-- Extra: Using filesort
-- 优化:添加联合索引
CREATE INDEX idx_name_age ON employees(name, age);
EXPLAIN SELECT * FROM employees ORDER BY name, age;
-- Extra: (无Using filesort,优化成功)
-- 问题查询:DISTINCT
EXPLAIN SELECT DISTINCT department_id FROM employees;
-- Extra: Using temporary
-- 优化:使用索引
CREATE INDEX idx_dept ON employees(department_id);
EXPLAIN SELECT department_id FROM employees;
-- Extra: Using index (优化成功)
优化策略:
- 为GROUP BY、ORDER BY、DISTINCT创建索引
- 使用覆盖索引避免临时表
- 限制返回的行数(LIMIT)
- 优化查询结构,减少排序需求
3.4 优化JOIN操作
示例:
sql
-- 问题查询:JOIN性能差
EXPLAIN SELECT e.*, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.age > 30;
-- type: ALL (employees表全表扫描)
-- 优化:添加索引
CREATE INDEX idx_age ON employees(age);
CREATE INDEX idx_dept ON employees(department_id);
EXPLAIN SELECT e.*, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.age > 30;
-- type: range (使用索引)
-- key: idx_age
优化策略:
- 为JOIN条件创建索引
- 为WHERE条件创建索引
- 小表驱动大表
- 避免JOIN过多表
四、最佳实践建议
4.1 模糊查询优化
sql
-- 避免
WHERE name LIKE '%张%'
-- 推荐
WHERE name LIKE '张%'
-- 或使用全文索引
WHERE MATCH(name) AGAINST('张')
4.2 函数操作优化
sql
-- 避免
WHERE YEAR(hire_date) = 2020
-- 推荐
WHERE hire_date BETWEEN '2020-01-01' AND '2020-12-31'
4.3 表达式计算优化
sql
-- 避免
WHERE salary + 1000 > 10000
-- 推荐
WHERE salary > 9000
4.4 类型转换优化
sql
-- 避免(phone是VARCHAR类型)
WHERE phone = 13800138001
-- 推荐
WHERE phone = '13800138001'
4.5 联合索引优化
sql
-- 联合索引:(name, age, department_id)
-- 避免
WHERE age = 25
-- 推荐
WHERE name = '张三' AND age = 25
4.6 OR条件优化
sql
-- 避免
WHERE name = '张三' OR age = 25
-- 推荐
WHERE name IN ('张三', '李四')
-- 或
SELECT * FROM employees WHERE name = '张三'
UNION
SELECT * FROM employees WHERE age = 25
五、总结
通过本文的学习,我们掌握了:
- EXPLAIN命令的基本用法:如何在SELECT语句前添加EXPLAIN来查看执行计划
- 12个输出字段的含义:从id到Extra,每个字段都代表查询执行的不同方面
- type字段的重要性:这是判断查询性能的关键指标,从system到ALL性能递减
- Extra字段的解读:Using index、Using temporary、Using filesort等信息的含义
- 性能优化策略:如何通过EXPLAIN识别性能瓶颈并进行优化
关键要点:
- 尽量避免type为ALL的全表扫描
- 优先使用range、ref、eq_ref等高效的访问类型
- 注意Extra中的Using temporary和Using filesort,这些是需要优化的信号
- 合理使用索引可以显著提升查询性能
延伸阅读:
如果你想了解如何使用EXPLAIN验证6种常见的索引失效场景,请阅读我的另一篇文章《MySQL EXPLAIN实战:6种索引失效场景验证与优化》。