MySQL技巧(四): EXPLAIN 关键参数详细解释

一、前言

1.1 什么是EXPLAIN

EXPLAIN是MySQL提供的SQL执行计划分析命令 ,用于展示MySQL优化器如何执行SQL语句。通过EXPLAIN可以分析索引使用情况、表连接顺序、扫描行数等关键信息,是SQL性能优化的核心工具

1.2 基础知识要求

  • SQL基础:了解基本的SELECT、JOIN语法

  • 索引概念:熟悉单列索引、联合索引、覆盖索引

  • 执行计划:理解MySQL优化器的工作方式


二、EXPLAIN的使用方法

2.1 基本语法

sql

sql 复制代码
-- 分析SELECT语句
EXPLAIN SELECT * FROM users WHERE id = 1;

-- 分析DELETE/UPDATE/INSERT(MySQL 5.6+)
EXPLAIN DELETE FROM users WHERE status = 'inactive';

-- 显示更详细信息(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;  -- 实际执行并显示耗时

-- 输出JSON格式(便于程序解析)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;

-- 查看连接中正在执行的SQL的执行计划
EXPLAIN FOR CONNECTION 123;  -- 123为connection_id

2.2 EXPLAIN输出字段概览

sql

sql 复制代码
EXPLAIN SELECT o.order_id, u.user_name 
FROM orders o 
LEFT JOIN users u ON o.user_id = u.user_id 
WHERE o.order_status = 'pending' 
  AND o.created_at >= '2024-01-01' 
LIMIT 10\G

输出结果示例

text

复制代码
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: o
   partitions: NULL
         type: range
possible_keys: idx_status_created,idx_created_at
          key: idx_status_created
      key_len: 102
          ref: NULL
         rows: 185000
     filtered: 100.00
        Extra: Using index condition
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: u
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test.o.user_id
         rows: 1
     filtered: 100.00
        Extra: NULL

三、核心参数详解

3.1 id:查询标识符

含义 示例场景
相同id 顺序执行,从上到下 多表JOIN
不同id id越大越先执行(优先级更高) 子查询、UNION

示例

sql

sql 复制代码
-- id相同:多表JOIN
EXPLAIN SELECT * FROM orders o, users u WHERE o.user_id = u.user_id;
-- 结果:两个id均为1

-- id不同:子查询
EXPLAIN SELECT * FROM users WHERE user_id IN (SELECT user_id FROM orders);
-- 结果:子查询id=2,外层查询id=1(子查询先执行)

3.2 select_type:查询类型

类型 含义 优化关注点
SIMPLE 简单查询,不包含子查询或UNION 最理想,无额外开销
PRIMARY 最外层查询 优化重点
SUBQUERY 子查询(非FROM子句) 尽量减少子查询,可改写为JOIN
DERIVED 派生表(FROM子句中的子查询) 会生成临时表,注意性能
UNION UNION中的第二个或后续查询 每个UNION分支单独分析
UNION RESULT UNION的结果集 合并结果的开销
DEPENDENT SUBQUERY 依赖外部查询的子查询 ⚠️ 高危,每行外层数据都会执行一次

重点关注

sql

sql 复制代码
-- 避免DEPENDENT SUBQUERY(子查询依赖外层)
EXPLAIN SELECT * FROM users u 
WHERE (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.user_id) > 5;
-- 优化:改为JOIN + GROUP BY

-- 注意DERIVED临时表开销
EXPLAIN SELECT * FROM (SELECT * FROM orders WHERE status='pending') t;
-- 建议:直接查询,或创建视图

3.3 type:访问类型(最重要指标之一

性能从优到劣排序:

text

复制代码
system > const > eq_ref > ref > range > index > ALL
类型 说明 示例 优化目标
system 系统表,只有一行数据 极少出现 ✅ 最优
const 主键或唯一索引等值查询,最多返回一行 WHERE id = 1 ✅ 理想
eq_ref 连接查询时,被驱动表使用主键或唯一索引 JOIN + 主键关联 ✅ 优秀
ref 使用非唯一索引等值查询 WHERE status = 'pending' ✅ 良好
range 索引范围扫描 WHERE id > 100BETWEENIN ✅ 可接受
index 全索引扫描(扫描整个索引树) 覆盖索引但无WHERE条件 ⚠️ 需优化
ALL 全表扫描 无索引或索引失效 必须优化

实战对比

sql

sql 复制代码
-- const:最优
EXPLAIN SELECT * FROM users WHERE user_id = 1;
-- type=const

-- ref:良好
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending';
-- type=ref(假设status有索引)

-- range:可接受
EXPLAIN SELECT * FROM orders WHERE created_at >= '2024-01-01';
-- type=range(假设created_at有索引)

-- ALL:必须优化
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending';
-- type=ALL(status无索引)

3.4 possible_keys:可能使用的索引

含义 处理建议
有索引名 优化器可能使用这些索引 正常
NULL 没有可用的索引 考虑添加索引

注意 :possible_keys有值不代表实际使用,需结合key字段判断。

3.5 key:实际使用的索引

含义 优化建议
有索引名 实际使用了该索引 良好
NULL 未使用索引(全表扫描或全索引扫描) 需要优化
PRIMARY 使用了主键索引 最优

关键场景

sql

sql 复制代码
-- possible_keys有值但key为NULL:索引未被使用
EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- possible_keys: idx_created_at, key: NULL
-- 原因:对索引字段使用了函数,索引失效

-- 实际使用了联合索引
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending' AND created_at > '2024-01-01';
-- key: idx_status_created(联合索引)

3.6 key_len:使用的索引字节长度

作用

  • 判断联合索引中实际使用了哪些字段

  • 长度越长,表示使用的索引字段越多

计算规则(以InnoDB为例):

数据类型 长度计算
TINYINT 1字节
INT 4字节
BIGINT 8字节
VARCHAR(100) 100*3 + 2(UTF8mb4)或 100 + 2(Latin1)
CHAR(10) 10*字符集字节数
允许NULL 额外+1字节

实战分析

sql

sql 复制代码
-- 联合索引:idx_status_created (order_status VARCHAR(20), created_at DATETIME)
SHOW INDEX FROM orders;  -- 查看索引定义

EXPLAIN SELECT * FROM orders 
WHERE order_status = 'pending' 
  AND created_at >= '2024-01-01';
-- key_len = 83
-- 计算:order_status(20*3+2=62) + created_at(8) + NULL标志(1) = 71? 实际83包含额外开销

-- 如果只使用第一个字段
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending';
-- key_len = 62(仅使用了order_status字段)

3.7 ref:索引列的比较对象

含义 示例
const 与常量比较 WHERE id = 1
表名.字段 与其他表字段比较 JOIN条件
NULL 非等值查询或未使用索引 WHERE id > 10

示例

sql

sql 复制代码
EXPLAIN SELECT * FROM orders o, users u 
WHERE o.user_id = u.user_id 
  AND o.order_status = 'pending';
-- table=o 的 ref: const (order_status='pending')
-- table=u 的 ref: test.o.user_id (关联到orders表的user_id)

3.8 rows:预估扫描行数

含义 :MySQL优化器预估需要读取的行数(非精确值

重要性

  • 核心优化指标,与查询耗时正相关

  • 目标:rows尽可能小

  • rows接近表总行数,说明索引效果差

实战

sql

sql 复制代码
-- 全表扫描:rows ≈ 表总行数
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending';
-- rows: 5,000,000(全表)

-- 添加索引后:rows大幅下降
EXPLAIN SELECT * FROM orders WHERE order_status = 'pending';
-- rows: 250,000(索引过滤后)

3.9 filtered:过滤后剩余行数百分比

含义 :满足WHERE条件的行数占rows预估百分比

计算实际返回行数 ≈ rows × filtered%

重要性

  • filtered越低,说明WHERE条件过滤效果好

  • filteredrows大时,需要更精准的索引

示例

sql

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE order_status = 'pending' 
  AND user_id = 100;
-- rows: 250000, filtered: 1.00
-- 实际返回行数 ≈ 2500(过滤掉了99%的数据)

-- 优化:创建(status, user_id)联合索引
-- 优化后 rows: 50, filtered: 100.00

3.10 Extra:额外信息(重要优化线索

含义 优化建议
Using index 覆盖索引,不需要回表 ✅ 最优状态
Using index condition 索引下推(ICP) ✅ 良好,MySQL 5.6+优化
Using where 使用WHERE过滤(在Server层) ⚠️ 可接受,但索引层过滤更优
Using filesort 需要额外排序,无法利用索引 需优化,添加排序字段索引
Using temporary 使用临时表(GROUP BY/UNION/DISTINCT) 需优化,通常是性能杀手
Using join buffer 连接缓冲区(Block Nested Loop) ⚠️ 被驱动表缺少索引
Using index for group-by 使用索引优化GROUP BY ✅ 良好
Impossible WHERE WHERE条件永远为假 检查业务逻辑
No tables used 没有FROM子句或FROM DUAL 正常

重点优化场景

场景1:Using filesort

sql

sql 复制代码
-- 问题SQL
EXPLAIN SELECT * FROM orders WHERE status='pending' ORDER BY created_at DESC;
-- Extra: Using where; Using filesort

-- 优化:添加(status, created_at)联合索引
CREATE INDEX idx_status_created ON orders(status, created_at);
-- 优化后Extra不再出现filesort
场景2:Using temporary

sql

sql 复制代码
-- 问题SQL
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY COUNT(*);
-- Extra: Using temporary; Using filesort

-- 优化:拆分为两个查询,或调整GROUP BY/ORDER BY顺序
场景3:Using index(覆盖索引)

sql

sql 复制代码
-- 覆盖索引:查询字段都在索引中
EXPLAIN SELECT order_id, user_id FROM orders WHERE order_id = 100;
-- Extra: Using index(主键索引覆盖)

-- 创建覆盖索引示例
CREATE INDEX idx_cover ON orders(status, created_at, order_id);
EXPLAIN SELECT status, created_at, order_id FROM orders WHERE status='pending';
-- Extra: Using index(无需回表)

四、实战分析流程

4.1 标准分析步骤

text

复制代码
1. 执行EXPLAIN,获取执行计划
   ↓
2. 检查type:是否为ALL或index?
   ↓ 是 → 添加索引
   ↓ 否
3. 检查key:是否为NULL?
   ↓ 是 → 分析索引失效原因
   ↓ 否
4. 检查rows:是否过大?
   ↓ 是 → 优化索引选择性
   ↓ 否
5. 检查Extra:是否有Using filesort/Using temporary?
   ↓ 是 → 优化排序/分组索引
   ↓ 否
6. 性能良好

4.2 综合案例分析

案例:订单报表查询慢

sql

sql 复制代码
EXPLAIN SELECT 
    DATE(o.created_at) as order_date,
    u.user_level,
    COUNT(*) as order_count,
    SUM(o.order_amount) as total_amount
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE o.created_at >= '2024-01-01'
  AND o.created_at < '2024-02-01'
  AND o.order_status IN ('paid', 'shipped')
GROUP BY DATE(o.created_at), u.user_level
ORDER BY order_date DESC, u.user_level;

EXPLAIN结果

table type key rows Extra
o ALL NULL 5,234,567 Using where; Using temporary; Using filesort
u eq_ref PRIMARY 1 NULL

问题诊断

  1. type=ALL:orders全表扫描

  2. key=NULL:未使用索引

  3. rows=523万:扫描全部数据

  4. Using temporary:GROUP BY产生临时表

  5. Using filesort:ORDER BY需要额外排序

优化方案

sql

sql 复制代码
-- 1. 创建联合索引
CREATE INDEX idx_status_created ON orders(order_status, created_at);

-- 2. 改写SQL,避免DATE()函数
SELECT 
    DATE(o.created_at) as order_date,
    u.user_level,
    COUNT(*) as order_count,
    SUM(o.order_amount) as total_amount
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE o.created_at >= '2024-01-01'
  AND o.created_at < '2024-02-01'
  AND o.order_status IN ('paid', 'shipped')
GROUP BY DATE(o.created_at), u.user_level
ORDER BY order_date DESC, u.user_level;

-- 3. 考虑使用汇总表(物化视图)预处理

五、EXPLAIN ANALYZE(MySQL 8.0.18+)

5.1 功能说明

实际执行SQL并返回详细的执行统计信息,包括实际耗时、实际行数等,比EXPLAIN更精确。

sql

sql 复制代码
EXPLAIN ANALYZE SELECT * FROM orders WHERE order_status = 'pending'\G

输出示例

text

复制代码
-> Filter: (orders.order_status = 'pending')  (cost=101.23 rows=1850) (actual time=0.123..0.456 rows=1234 loops=1)
    -> Index lookup on orders using idx_status (order_status='pending')  (cost=101.23 rows=1850) (actual time=0.098..0.234 rows=1234 loops=1)

关键信息

  • actual time:实际执行时间

  • rows:实际返回行数

  • loops:循环执行次数(被驱动表)


六、优化检查清单

检查项 理想状态 问题信号
type const/eq_ref/ref/range ALL/index
key 有索引名 NULL
rows < 1000或占总行数<5% 接近表总行数
Extra Using index Using filesort/Using temporary
filtered 高(>30%) 低(<5%)但rows大

七、学习建议

  1. 熟记type优先级:const > eq_ref > ref > range > index > ALL

  2. 重点关注Extra:filesort和temporary是常见性能杀手

  3. 结合业务验证rows:预估扫描行数是否合理

  4. 善用SHOW INDEX:了解表索引结构后再分析

  5. MySQL 8.0用EXPLAIN ANALYZE:获取实际执行统计

  6. 建立知识库:记录常见问题模式(函数导致索引失效、隐式类型转换等)

相关推荐
gjc5922 小时前
踩坑实录:MySQL服务器CPU爆高,元凶竟是SELinux的setroubleshootd?
运维·服务器·数据库·mysql·adb
没有了遇见2 小时前
Android 架构之网络框架多域名配置<三>
android
yhole3 小时前
MySQL无法连接到本地localhost的解决办法2024.11.8
数据库·mysql·adb
guslegend4 小时前
MySQL高手第一章
mysql·adb
myloveasuka4 小时前
[Java]单列集合
android·java·开发语言
fundroid4 小时前
Room 3.0 完全解析:一次面向未来的现代化重构
android·数据库·database·kmp
漂洋过海来看你啊4 小时前
Jetpack Compose高效列表实战:状态管理与性能优化指南
android
张宏2365 小时前
android camera hal3-camera_module_t
android
hongtianzai5 小时前
Laravel9.X核心特性全解析
android·java·数据库