MySQL EXPLAIN命令详解:SQL查询性能分析与优化指南(基础篇)

前言 :在数据库开发和优化过程中,SQL查询的性能直接影响应用的响应速度和用户体验。一个看似简单的查询,如果执行计划不当,可能会导致全表扫描、临时表创建、文件排序等性能问题。MySQL提供了EXPLAIN命令,让我们能够查看SQL查询的执行计划,了解数据库如何执行查询,从而识别性能瓶颈并进行优化。

本文将深入讲解:

  • EXPLAIN命令的作用和基本用法
  • EXPLAIN输出字段的详细含义
  • 如何通过EXPLAIN分析SQL性能瓶颈
  • SQL查询性能优化的实战技巧

一、EXPLAIN命令概述

1.1 什么是执行计划?

SQL执行计划展示了数据库如何执行查询的详细过程,包括:

  • 使用了哪些索引
  • 访问表的顺序
  • MySQL做了哪些优化
  • 预计扫描的行数
  • 是否使用临时表或外部排序

通过分析执行计划,我们可以:

  • 识别全表扫描
  • 发现未使用的索引
  • 定位性能瓶颈
  • 优化查询结构

1.2 EXPLAIN关键字的使用指南

EXPLAIN关键字 用于显示MySQL查询的执行计划,可以帮助我们分析查询语句的效率。执行后会返回一个表格,表格的字段包括:idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra等。每个字段代表查询执行的不同方面,理解这些字段有助于我们优化查询。

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

优化策略:

  1. 添加合适的索引
  2. 修改查询条件(如右模糊匹配)
  3. 使用覆盖索引
  4. 考虑使用全文索引或搜索引擎

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

优化策略:

  1. 使用SHOW INDEX查看现有索引
  2. 根据查询条件添加合适的索引
  3. 定期清理无用索引
  4. 使用索引提示(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 (优化成功)

优化策略:

  1. 为GROUP BY、ORDER BY、DISTINCT创建索引
  2. 使用覆盖索引避免临时表
  3. 限制返回的行数(LIMIT)
  4. 优化查询结构,减少排序需求

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

优化策略:

  1. 为JOIN条件创建索引
  2. 为WHERE条件创建索引
  3. 小表驱动大表
  4. 避免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

五、总结

通过本文的学习,我们掌握了:

  1. EXPLAIN命令的基本用法:如何在SELECT语句前添加EXPLAIN来查看执行计划
  2. 12个输出字段的含义:从id到Extra,每个字段都代表查询执行的不同方面
  3. type字段的重要性:这是判断查询性能的关键指标,从system到ALL性能递减
  4. Extra字段的解读:Using index、Using temporary、Using filesort等信息的含义
  5. 性能优化策略:如何通过EXPLAIN识别性能瓶颈并进行优化

关键要点:

  • 尽量避免type为ALL的全表扫描
  • 优先使用range、ref、eq_ref等高效的访问类型
  • 注意Extra中的Using temporary和Using filesort,这些是需要优化的信号
  • 合理使用索引可以显著提升查询性能

延伸阅读:

如果你想了解如何使用EXPLAIN验证6种常见的索引失效场景,请阅读我的另一篇文章《MySQL EXPLAIN实战:6种索引失效场景验证与优化》。


相关推荐
禹凕2 小时前
MYSQL——基础知识(MYSQL 索引)
数据库·mysql
Zhu_S W2 小时前
MySQL大表优化完全指南
数据库·mysql
CN-David2 小时前
CentOS搭建Mycat中间件
linux·mysql·中间件·centos·mariadb
kyle~2 小时前
MySQL基础知识点与常用SQL语句整理
android·sql·mysql
青衫码上行2 小时前
高频SQL 50题 | 聚合
数据库·sql·mysql·leetcode·面试
有点心急10212 小时前
SQL 执行 MCP 工具开发(二)
数据库·sql
m0_528749003 小时前
复杂一点的sql查询
数据库·sql
崎岖Qiu3 小时前
使用 Redis 的 List 实现缓存分页信息(模拟 limit offset 的 SQL 语句)
redis·mysql·缓存·list
_OP_CHEN5 小时前
【MySQL数据库基础】(一)保姆级 MySQL 环境配置教程!CentOS 7+Ubuntu 双系统全覆盖
linux·数据库·sql·mysql·ubuntu·centos·环境配置