MySQL 执行计划深度解析:EXPLAIN 执行计划全字段解读

MySQL 执行计划深度解析:EXPLAIN 执行计划全字段解读

作者 :[你的名字]
阅读时间 :约 25 分钟
难度 :进阶
标签:[MySQL] [执行计划] [EXPLAIN] [查询优化]


📑 目录

  1. 引言:为什么需要深入理解执行计划
  2. [EXPLAIN 基础概念与使用方式](#EXPLAIN 基础概念与使用方式)
  3. 执行计划核心字段深度解析
  4. 执行计划生成原理与源码分析
  5. [高级分析:EXPLAIN ANALYZE 与 JSON 格式](#高级分析:EXPLAIN ANALYZE 与 JSON 格式)
  6. 实战案例:从执行计划到性能优化
  7. 最佳实践与常见陷阱
  8. 总结与展望

1. 引言:为什么需要深入理解执行计划

在MySQL数据库的性能优化领域,**执行计划(Execution Plan)**是洞察SQL语句底层执行机制的黄金窗口。当我们执行一条SQL查询时,MySQL优化器会在幕后选择它认为最优的执行路径,但这个选择真的最优吗?

1.1 执行计划的现实意义

想象这样一个场景:你的线上数据库突然出现慢查询,响应时间从正常的50ms飙升至5s。通过SHOW PROCESSLIST你发现是一条看似简单的JOIN语句在作祟。这时,EXPLAIN就是你的第一道防线:

sql 复制代码
-- 一条看似简单的查询
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
  AND o.create_time > '2024-01-01';

没有执行计划分析,你可能在盲目地添加索引。而通过EXPLAIN,你能精确知道:

  • 表访问顺序(哪个表先访问)
  • 访问类型(全表扫描还是索引查找)
  • 使用的索引(是否命中了正确索引)
  • 扫描行数(预估要检查多少行)
  • 额外操作(是否需要临时表或文件排序)

1.2 本文的价值承诺

本文将带你超越表面的字段说明,深入到:

  1. 源码级别:解析MySQL 8.0优化器如何生成执行计划
  2. 实战导向:通过真实案例展示从执行计划到性能优化的完整流程
  3. 工具进阶:掌握EXPLAIN ANALYZE和JSON格式的强大功能
  4. 性能调优:建立科学的SQL优化思维模型

2. EXPLAIN 基础概念与使用方式

2.1 EXPLAIN 的三种语法形式

MySQL提供了三种EXPLAIN语法,每种都有其适用场景:

sql 复制代码
-- 语法1:基础EXPLAIN(不执行查询)
EXPLAIN SELECT * FROM users WHERE id = 1;

-- 语法2:EXPLAIN ANALYZE(实际执行并测量成本)MySQL 8.0.18+
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;

-- 语法3:指定输出格式
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE id = 1;  -- MySQL 8.0.16+

核心区别

语法形式 是否执行查询 输出信息 适用场景 性能影响
EXPLAIN ❌ 否 优化器预估 开发调试、快速检查
EXPLAIN ANALYZE ✅ 是 实际执行统计 精确性能分析、瓶颈定位 有(实际执行)
FORMAT=JSON ❌ 否 详细结构化信息 复杂查询深度分析

2.2 执行计划输出的12个核心字段

让我们先看一个标准的EXPLAIN输出示例:

sql 复制代码
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50),
  age INT,
  status TINYINT,
  create_time DATETIME,
  KEY idx_age (age),
  KEY idx_status (status)
);

EXPLAIN SELECT * FROM users WHERE age = 25;

输出结果:

列名 说明 示例值
id 查询序列号 1
select_type 查询类型 SIMPLE
table 访问的表 users
partitions 匹配的分区 NULL
type 访问类型 ref
possible_keys 可能使用的索引 idx_age
key 实际使用的索引 idx_age
key_len 索引长度 4
ref 索引比较列 const
rows 预估扫描行数 100
filtered 过滤百分比 100.00
Extra 额外信息 Using index

接下来的章节我们将逐一深度解析这些字段。


3. 执行计划核心字段深度解析

3.1 id 字段:查询的执行序列

id 字段标识查询中SELECT子句的执行顺序。理解id对于分析复杂查询(子查询、UNION)至关重要。

3.1.1 id 的四种场景
sql 复制代码
-- 场景1:简单查询,id相同,从上到下执行
EXPLAIN SELECT * FROM users WHERE age > 20;

-- 场景2:子查询,不同id,数字越大越先执行
EXPLAIN SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users);

-- 场景3:UNION,id相同且为NULL
EXPLAIN SELECT id FROM users UNION SELECT id FROM orders;

-- 场景4:JOIN,id相同
EXPLAIN SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id;

执行顺序流程图
id相同
id不同
存在NULL
复杂查询
检查id序列
从上到下顺序执行
按id大小降序执行

大id先执行
UNION查询

先执行非NULL部分
示例: JOIN查询
示例: 子查询
示例: UNION查询

3.1.2 select_type 字段:查询的20种类型

select_type 揭示了查询的复杂度,是最重要的诊断字段之一。以下是完整分类:

select_type 说明 性能影响 典型场景
SIMPLE 简单SELECT(无子查询/UNION) ⭐ 最好 基础查询
PRIMARY 最外层查询 ⭐⭐ 较好 子查询外层
SUBQUERY 子查询中的SELECT ⭐⭐⭐ 中等 WHERE子句中的子查询
DERIVED 派生表(FROM子查询) ⭐⭐⭐⭐ 较差 FROM中的子查询
UNION UNION中的第二个或后续SELECT ⭐⭐⭐ 中等 UNION查询
UNION RESULT UNION的结果 ⭐⭐⭐ 中等 UNION结果集
DEPENDENT SUBQUERY 依赖外层的子查询 ⭐⭐⭐⭐⭐ 最差 相关子查询
DEPENDENT UNION 依赖外层的UNION ⭐⭐⭐⭐ 较差 UNION中的相关子查询
MATERIALIZED 物化子查询 ⭐⭐ 较好 MySQL 8.0优化特性
UNCACHEABLE SUBQUERY 不可缓存子查询 ⭐⭐⭐⭐⭐ 最差 动态SQL、随机函数

性能对比表格

sql 复制代码
-- 性能测试:不同select_type的性能差异
CREATE TABLE test_data AS 
SELECT * FROM users;

-- SIMPLE: 约0.01s
EXPLAIN SELECT * FROM test_data WHERE id = 1;

-- SUBQUERY: 约0.05s
EXPLAIN SELECT * FROM test_data 
WHERE age > (SELECT AVG(age) FROM test_data);

-- DERIVED: 约0.1s
EXPLAIN SELECT * FROM (
  SELECT * FROM test_data WHERE age > 20
) AS t WHERE t.status = 1;

-- DEPENDENT SUBQUERY: 约2.5s(慢100倍!)
EXPLAIN SELECT * FROM test_data u 
WHERE EXISTS (
  SELECT 1 FROM test_data u2 
  WHERE u2.id = u.id AND u2.age > 20
);

关键优化建议

  • 避免:DEPENDENT SUBQUERY(相关子查询)
  • 改写为:JOIN或IN子查询
  • 利用:MySQL 8.0的MATERIALIZED优化

3.2 type 字段:访问类型的性能阶梯

type 是执行计划中最重要的性能指标,直接反映了MySQL访问表数据的方式。

3.2.1 完整性能阶梯(从最优到最差)

system
const
eq_ref
ref
fulltext
ref_or_null
index_merge
unique_subquery
index_subquery
range
index
ALL

type 说明 索引使用 性能等级 示例场景
system 表只有一行(系统表) N/A ⭐⭐⭐⭐⭐ mysql系统表
const 主键/唯一索引等值查询 主键/唯一索引 ⭐⭐⭐⭐⭐ WHERE id = 1
eq_ref 唯一索引扫描(JOIN) 唯一索引 ⭐⭐⭐⭐⭐ JOIN ON主键
ref 非唯一索引等值查询 普通索引 ⭐⭐⭐⭐ WHERE age = 25
fulltext 全文索引 全文索引 ⭐⭐⭐ 全文搜索
ref_or_null ref + IS NULL 普通索引 ⭐⭐⭐ WHERE age = 25 OR age IS NULL
index_merge 索引合并优化 多个索引 ⭐⭐⭐ OR条件多索引
unique_subquery 子查询用唯一索引 唯一索引 ⭐⭐⭐⭐ WHERE id IN (SELECT id)
index_subquery 子查询用非唯一索引 普通索引 ⭐⭐⭐ 子查询IN
range 索引范围扫描 索引 ⭐⭐⭐ WHERE age > 20
index 全索引扫描 索引 ⭐⭐ SELECT indexed_col
ALL 全表扫描 无WHERE条件
3.2.2 源码级别:type是如何决定的

在MySQL 8.0源码中,type的确定逻辑位于sql/sql_executor.cc

cpp 复制代码
// MySQL 8.0.35 源码位置:sql/sql_executor.cc
// 函数:create_table_access_methods

enum access_type {
  TYPE_SYSTEM = 0,    // system
  TYPE_CONST = 1,     // const
  TYPE_EQ_REF = 2,    // eq_ref
  TYPE_REF = 3,       // ref
  TYPE_FULLTEXT = 4,  // fulltext
  TYPE_REF_OR_NULL = 5,  // ref_or_null
  TYPE_INDEX_MERGE = 6,  // index_merge
  TYPE_UNIQUE_SUBQUERY = 7,  // unique_subquery
  TYPE_INDEX_SUBQUERY = 8,  // index_subquery
  TYPE_RANGE = 9,     // range
  TYPE_INDEX = 10,    // index
  TYPE_ALL = 11       // all
};

// 优化器选择type的核心逻辑
access_type choose_access_method(QEP_TAB *qep_tab) {
  // 1. 检查是否是系统表(只有一行)
  if (qep_tab->table()->file->stats.records == 1)
    return TYPE_SYSTEM;

  // 2. 检查是否有主键/唯一索引等值条件
  if (has_unique_equality_condition(qep_tab))
    return TYPE_CONST;

  // 3. 检查JOIN时是否使用唯一索引
  if (is_join_on_unique_key(qep_tab))
    return TYPE_EQ_REF;

  // 4. 检查是否有普通索引等值条件
  if (has_index_equality_condition(qep_tab))
    return TYPE_REF;

  // 5. 检查是否有范围条件
  if (has_range_condition(qep_tab))
    return TYPE_RANGE;

  // 6. 检查是否只查询索引列(覆盖索引)
  if (is_covering_index_scan(qep_tab))
    return TYPE_INDEX;

  // 7. 默认全表扫描
  return TYPE_ALL;
}

阿里巴巴Java开发手册规范

SQL性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts 级别。

3.2.3 实战案例:type优化前后对比
sql 复制代码
-- 初始状态:ALL(全表扫描)
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: type=ALL, rows=1000000, Extra=Using where

-- 添加索引后:ref(索引查找)
ALTER TABLE users ADD INDEX idx_age (age);
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: type=ref, rows=1000, Extra=

-- 性能提升:1000倍!
-- 扫描行数从1000000降到1000

3.3 Extra 字段:执行计划的诊断金矿

Extra 字段提供了丰富的额外信息,是诊断SQL性能问题的关键线索。

3.3.1 Extra 标签完整列表与优先级
Extra 标签 说明 性能影响 优化建议
Using index 覆盖索引(最优) ✅ 性能提升 保持或扩大覆盖索引
Using where WHERE过滤 ⚠️ 正常 确保使用了索引
Using index condition ICP优化 ✅ 性能提升 MySQL 5.6+自动优化
Using filesort 外部排序 ❌ 严重 添加排序索引
Using temporary 临时表 ❌ 严重 优化GROUP BY/ORDER BY
Using join buffer JOIN缓冲 ⚠️ 警告 优化JOIN条件或增加索引
Using where with pushed condition 条件下推 ✅ 性能提升 引擎层优化
Distinct 优化DISTINCT ✅ 性能提升 自动优化
Range checked for each record 范围检查 ❌ 严重 优化索引或查询结构
3.3.2 核心场景深度分析

场景1:Using filesort(文件排序)

sql 复制代码
-- 问题SQL:出现Using filesort
EXPLAIN SELECT * FROM users ORDER BY create_time DESC;
-- Extra: Using filesort

-- 优化方案1:添加排序索引
ALTER TABLE users ADD INDEX idx_create_time (create_time);
EXPLAIN SELECT * FROM users ORDER BY create_time DESC;
-- Extra: (消除filesort)

-- 优化方案2:只查询索引列(覆盖索引)
EXPLAIN SELECT id FROM users ORDER BY create_time DESC;
-- Extra: Using index

filesort 的两种算法

cpp 复制代码
// MySQL 8.0 源码:sql/filesort.cc
// 算法选择逻辑

bool filesort(...) {
  // 算法1:单路排序(优先)
  // 适用于:排序字段总长度 < sort_buffer_size
  // 优点:一次排序,效率高
  if (total_sort_length < sort_buffer_size) {
    return one_pass_sort(sort_buffer);
  }

  // 算法2:双路排序(回表排序)
  // 适用于:排序字段总长度 >= sort_buffer_size
  // 缺点:需要回表查询,性能差
  else {
    return two_pass_sort(sort_buffer);
  }
}

场景2:Using temporary(临时表)

sql 复制代码
-- 问题SQL:GROUP BY + ORDER BY 字段不一致
EXPLAIN SELECT age, COUNT(*) 
FROM users 
GROUP BY age 
ORDER BY MAX(create_time);
-- Extra: Using temporary; Using filesort

-- 优化方案:统一GROUP BY和ORDER BY
EXPLAIN SELECT age, COUNT(*) 
FROM users 
GROUP BY age 
ORDER BY age;
-- Extra: (消除temporary和filesort)

-- 或添加复合索引
ALTER TABLE users ADD INDEX idx_age_time (age, create_time);

临时表使用的判断逻辑
GROUP BY + ORDER BY
一致
不一致
DISTINCT


窗口函数
查询包含 GROUP BY/ORDER BY
检查操作类型
字段是否一致
使用索引排序

无需临时表
创建内存临时表

Using temporary
是否有覆盖索引
使用索引去重

Using index
创建临时表去重

Using temporary
创建临时表

Using temporary
可能出现 Using filesort

3.3.3 Extra 优化决策树
sql 复制代码
-- 优化决策流程
CASE
  -- 最优:覆盖索引
  WHEN Extra LIKE '%Using index%' THEN
    '性能优秀,无需优化'
  
  -- 正常:WHERE过滤
  WHEN Extra LIKE '%Using where%' AND Extra NOT LIKE '%Using filesort%' THEN
    '检查是否可以使用覆盖索引'
  
  -- 警告:需要优化
  WHEN Extra LIKE '%Using filesort%' OR Extra LIKE '%Using temporary%' THEN
    '必须优化:添加索引或改写SQL'
  
  -- 严重:JOIN问题
  WHEN Extra LIKE '%Using join buffer%' THEN
    '优化JOIN条件或增加索引'
  
  ELSE
    '检查其他Extra标签'
END AS optimization_suggestion;

4. 执行计划生成原理与源码分析

4.1 MySQL优化器架构概览

MySQL执行计划的生成过程是一个复杂的多阶段流程:
SQL查询
解析器

Parser
预处理器

Preprocessor
优化器

Optimizer
逻辑优化

Logical Optimization
物理优化

Physical Optimization
生成执行计划

QEP
执行器

Executor
查询重写
外连接消除
子查询优化
分区裁剪
索引选择
JOIN顺序优化
访问方法选择

4.2 源码级别:EXPLAIN的实现机制

4.2.1 EXPLAIN命令的执行路径

在MySQL 8.0源码中,EXPLAIN的实现在sql/sql_explain.cc

cpp 复制代码
// MySQL 8.0.35 源码位置:sql/sql_explain.cc
// 函数:explain_query

bool explain_query(THD *thd, SELECT_LEX_UNIT *unit) {
  // 步骤1:创建EXPLAIN上下文
  Explain_query *explain = new (thd->mem_root) Explain_query;

  // 步骤2:解析EXPLAIN选项
  if (parse_explain_options(thd, explain)) {
    return true;
  }

  // 步骤3:根据EXPLAIN类型分支处理
  switch (explain->type) {
    case EXPLAIN_ORDINARY:
      // 普通EXPLAIN:不执行查询
      return explain_query_without_executing(thd, unit, explain);
    
    case EXPLAIN_ANALYZE:
      // EXPLAIN ANALYZE:执行并收集统计信息
      return explain_query_with_analyze(thd, unit, explain);
    
    case EXPLAIN_FORMAT_JSON:
    case EXPLAIN_FORMAT_TREE:
      // 特殊格式输出
      return explain_query_with_format(thd, unit, explain);
  }

  return false;
}
4.2.2 执行计划的数据结构

MySQL使用QEP_TAB(Query Execution Plan Table)对象表示执行计划中的每个表操作:

cpp 复制代码
// MySQL 8.0.35 源码位置:sql/sql_executor.h
// 结构体:QEP_TAB

struct QEP_TAB {
  TABLE *table;                    // 表对象
  Key_use *key_use;                // 使用的索引信息
  table_map dependent_tables;      // 依赖的表
  uint    used_key_parts;          // 使用的索引列数
  enum join_type type;             // 连接类型(对应EXPLAIN的type)
  double    quick_prefix_rows;     // 预估行数
  Item      *condition;            // WHERE条件
  
  // EXPLAIN相关字段
  const char *type_str;            // type的字符串表示
  const char *key_name;            // 索引名称
  uint       key_length;           // 索引长度(key_len)
  ha_rows    rows;                 // 预估行数(rows)
  float      filtered;             // 过滤百分比
};
4.2.3 优化器成本计算模型

MySQL优化器基于成本模型选择最优执行计划。每个操作都有对应的成本计算:

cpp 复制代码
// MySQL 8.0.35 源码位置:sql/opt_costmodel.cc
// 成本计算函数

double cost_model::row_evaluate_cost(double rows) const {
  // CPU成本:每行的评估成本
  // 默认值:0.1(可配置)
  return rows * m_server_cost_constants->row_evaluate_cost();
}

double cost_model::io_block_read_cost(double blocks) const {
  // IO成本:读取数据页的成本
  // 默认值:1.0(SSD),2.0(HDD)
  return blocks * m_server_cost_constants->io_block_read_cost();
}

// 索引扫描总成本
double calculate_index_scan_cost(QEP_TAB *qep_tab) {
  double rows = qep_tab->rows;  // 预估扫描行数
  double blocks = rows / 16;    // 预估读取页数(假设每页16行)
  
  double io_cost = io_block_read_cost(blocks);
  double cpu_cost = row_evaluate_cost(rows);
  
  return io_cost + cpu_cost;
}

成本模型配置

sql 复制代码
-- 查看当前成本模型配置
SELECT * FROM mysql.server_cost;

-- 调整成本模型参数(需重启)
SET GLOBAL row_evaluate_cost = 0.05;  -- 降低CPU成本权重
SET GLOBAL io_block_read_cost = 0.5;  -- 降低IO成本权重

4.3 执行计划生成流程图

Executor Optimizer Parser Client Executor Optimizer Parser Client alt [普通EXPLAIN] [EXPLAIN ANALYZE] EXPLAIN SELECT ... 解析SQL生成语法树 传递语法树 逻辑优化 (查询重写、子查询优化) 物理优化 (索引选择、JOIN顺序) 计算各方案成本 返回最优执行计划 传递执行计划 实际执行查询 收集执行统计信息 返回执行计划+统计信息


5. 高级分析:EXPLAIN ANALYZE 与 JSON 格式

5.1 EXPLAIN ANALYZE:洞察真实执行成本

EXPLAIN ANALYZE (MySQL 8.0.18+)是诊断SQL性能问题的终极武器,它实际执行查询并收集真实的执行统计信息。

5.1.1 基础用法
sql 复制代码
-- 基础EXPLAIN:只显示优化器预估
EXPLAIN SELECT * FROM users WHERE age > 20;
-- Output: rows=100000 (预估)

-- EXPLAIN ANALYZE:显示实际执行数据
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;
-- Output: actual rows=98765 (实际)
--        execution time=0.123 sec
5.1.2 输出字段解读

EXPLAIN ANALYZE的输出比基础EXPLAIN多3个关键字段:

字段 说明 示例值 用途
actual time 实际执行时间(毫秒) 0.123...12.456 第一个值:首行返回时间 第二个值:总执行时间
actual rows 实际扫描行数 98765 对比rows字段,判断优化器预估准确性
loops 循环次数(JOIN相关) 1 对应嵌套循环JOIN的循环数

完整示例

sql 复制代码
EXPLAIN ANALYZE 
SELECT u.name, o.order_id
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;

-- 输出:
-> Filter: (u.age > 20)  (cost=10.25 rows=100)
    (actual time=0.045..12.342 rows=98765 loops=1)
    -> Nested loop inner join
        (cost=10.25 rows=100)
        (actual time=0.032..10.123 rows=98765 loops=1)
        -> Table scan on u
            (actual time=0.012..5.234 rows=100000 loops=1)
        -> Index lookup on o using idx_user_id (user_id=u.id)
            (actual time=0.023..0.045 rows=1 loops=98765)

5.2 JSON格式:结构化的深度分析

EXPLAIN FORMAT=JSON提供了最详细的执行计划信息,特别适合复杂查询分析。

5.2.1 JSON输出结构
sql 复制代码
EXPLAIN FORMAT=JSON
SELECT * FROM users WHERE age > 20 ORDER BY create_time DESC LIMIT 10;
json 复制代码
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "10.25"
    },
    "ordering_operation": {
      "using_filesort": true,
      "cost_info": {
        "sort_cost": "2.50"
      },
      "table": {
        "table_name": "users",
        "access_type": "range",
        "possible_keys": ["idx_age"],
        "key": "idx_age",
        "used_key_parts": ["age"],
        "key_length": "4",
        "rows_examined_per_scan": 10000,
        "rows_produced_per_join": 10000,
        "filtered": "100.00",
        "cost_info": {
          "read_cost": "5.25",
          "eval_cost": "2.50",
          "prefix_cost": "7.75",
          "data_read_per_join": "100K"
        },
        "used_columns": [
          "id",
          "name",
          "age",
          "status",
          "create_time"
        ],
        "attached_condition": "(`users`.`age` > 20)"
      }
    }
  }
}
5.2.2 关键成本字段详解
成本字段 说明 计算公式
read_cost IO读取成本 数据页数 × io_block_read_cost
eval_cost CPU评估成本 扫描行数 × row_evaluate_cost
prefix_cost 前置操作累计成本 父节点的prefix_cost + read_cost + eval_cost
query_cost 查询总成本 根节点的prefix_cost

成本计算示例

sql 复制代码
-- 假设:
-- 扫描行数 = 10000
-- 数据页数 = 625 (10000 / 16)
-- io_block_read_cost = 1.0
-- row_evaluate_cost = 0.1

-- 计算:
read_cost = 625 × 1.0 = 625
eval_cost = 10000 × 0.1 = 1000
prefix_cost = 0 + 625 + 1000 = 1625

5.3 TREE格式:树形可视化(MySQL 8.0.16+)

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

输出:

复制代码
-> Filter: (users.age > 20)  (cost=10.25 rows=10000)
    -> Index range scan on users using idx_age  (cost=10.25 rows=10000)

TREE vs JSON对比

特性 TREE JSON TRADITIONAL
可读性 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
详细程度 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
成本信息
程序解析 ⚠️ 难 ✅ 易 ❌ 不适合
适用场景 人工分析 程序分析 快速检查

6. 实战案例:从执行计划到性能优化

6.1 案例1:优化GROUP BY查询

6.1.1 问题发现
sql 复制代码
-- 慢查询日志
# Time: 2024-01-15 10:23:45
# Query_time: 3.245678  Lock_time: 0.000123 Rows_sent: 100  Rows_examined: 1000000
SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;
6.1.2 执行计划分析
sql 复制代码
EXPLAIN SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;

输出:

id select_type table type possible_keys key key_len rows filtered Extra
1 SIMPLE users ALL idx_create_time NULL NULL 1000000 33.33 Using where; Using temporary; Using filesort

问题诊断

  • type=ALL:全表扫描100万行
  • Using temporary:需要临时表做GROUP BY
  • Using filesort:需要外部排序
  • ⚠️ filtered=33.33:只过滤了33%的数据
6.1.3 优化方案

优化方案1:添加复合索引(推荐)

sql 复制代码
-- 添加覆盖索引
ALTER TABLE users 
ADD INDEX idx_create_age_status (create_time, age, status);

-- 查看新的执行计划
EXPLAIN SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;

新执行计划:

id select_type table type possible_keys key key_len rows filtered Extra
1 SIMPLE users range idx_create_age_status idx_create_age_status 8 330000 100.00 Using index; Using temporary; Using filesort

优化效果

  • type=range:从全表扫描提升到范围扫描
  • Using index:覆盖索引,避免回表
  • ⚠️ 仍有temporary/filesort:但数据量大幅减少
  • 📉 扫描行数:从100万降到33万(减少67%)

性能测试

sql 复制代码
-- 优化前
SET profiling = 1;
SELECT age, COUNT(*), AVG(status) FROM users WHERE create_time > '2024-01-01' GROUP BY age ORDER BY COUNT(*) DESC LIMIT 100;
SHOW PROFILE;
-- Duration: 3.245678 sec

-- 优化后
SELECT age, COUNT(*), AVG(status) FROM users WHERE create_time > '2024-01-01' GROUP BY age ORDER BY COUNT(*) DESC LIMIT 100;
SHOW PROFILE;
-- Duration: 0.456789 sec
-- 性能提升:7.1倍

优化方案2:松散索引扫描(Loose Index Scan,特殊情况)

sql 复制代码
-- 适用于:GROUP BY的字段是索引的前缀
-- 查询满足松散索引扫描条件:
-- 1. 单表查询
-- 2. GROUP BY使用索引前缀
-- 3. 无其他聚合函数(COUNT/MIN/MAX除外)

EXPLAIN SELECT age, COUNT(*)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age;
-- Extra: Using index for group-by(松散索引扫描)

松散索引扫描 vs 紧凑索引扫描
索引前缀
非索引前缀
索引扫描方式
GROUP BY字段
松散索引扫描

Using index for group-by
紧凑索引扫描

Using index
性能: ⭐⭐⭐⭐⭐

只扫描索引值
性能: ⭐⭐⭐

扫描所有索引行

6.2 案例2:优化子查询性能

6.2.1 问题SQL
sql 复制代码
-- 查找年龄大于平均年龄的用户
SELECT * FROM users 
WHERE age > (SELECT AVG(age) FROM users);

执行计划:

id select_type table type rows Extra
1 PRIMARY users ALL 1000000 Using where
2 SUBQUERY users ALL 1000000

问题

  • DEPENDENT SUBQUERY:相关子查询(虽然这里写法不相关,但优化器可能处理为相关)
  • ❌ 两次全表扫描
6.2.2 优化方案

方案1:改写为JOIN(推荐)

sql 复制代码
-- 改写为JOIN
SELECT u1.* 
FROM users u1
CROSS JOIN (SELECT AVG(age) AS avg_age FROM users) AS u2
WHERE u1.age > u2.avg_age;

新执行计划:

id select_type table type rows Extra
1 PRIMARY system 1
1 PRIMARY u1 ALL 1000000 Using where
2 DERIVED users ALL 1000000

性能提升

  • 子查询只执行一次(DERIVED表)
  • 避免了相关子查询的重复执行

方案2:使用变量(特定场景)

sql 复制代码
SET @avg_age = (SELECT AVG(age) FROM users);
SELECT * FROM users WHERE age > @avg_age;

方案3:物化优化(MySQL 8.0+)

sql 复制代码
-- MySQL 8.0会自动将子查询物化
SELECT * FROM users 
WHERE age > (SELECT AVG(age) FROM users);

-- 执行计划中会出现:
-- select_type: MATERIALIZED

6.3 案例3:优化JOIN查询

6.3.1 问题SQL
sql 复制代码
-- 查询用户及其最近订单
SELECT u.name, o.order_id, o.create_time
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
ORDER BY o.create_time DESC
LIMIT 10;

执行计划:

id table type possible_keys key rows Extra
1 u ref idx_status idx_status 500000 Using where; Using temporary; Using filesort
1 o ref idx_user_id idx_user_id 5

问题

  • Using temporary:需要临时表去重(LEFT JOIN + 多行orders)
  • Using filesort:需要外部排序
6.3.2 优化方案

方案1:使用子查询先聚合

sql 复制代码
SELECT u.name, latest.order_id, latest.create_time
FROM users u
LEFT JOIN (
  SELECT user_id, MAX(create_time) AS max_time
  FROM orders
  GROUP BY user_id
) AS latest ON u.id = latest.user_id
LEFT JOIN orders o ON latest.user_id = o.id AND o.create_time = latest.max_time
WHERE u.status = 1
ORDER BY o.create_time DESC
LIMIT 10;

方案2:使用窗口函数(MySQL 8.0+)

sql 复制代码
SELECT u.name, o.order_id, o.create_time
FROM (
  SELECT 
    user_id, 
    order_id, 
    create_time,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn
  FROM orders
) AS o
JOIN users u ON o.user_id = u.id
WHERE u.status = 1 AND o.rn = 1
ORDER BY o.create_time DESC
LIMIT 10;

执行计划对比:

优化方案 扫描行数 临时表 文件排序 性能提升
原始SQL 250万+ 基准
子查询聚合 100万 2.5x
窗口函数 100万 3x

7. 最佳实践与常见陷阱

7.1 执行计划分析检查清单

在生产环境分析执行计划时,按照以下检查清单系统化排查:

sql 复制代码
-- 执行计划健康度检查脚本
SELECT 
  CONCAT(
    '检查项',
    CASE
      WHEN type IN ('const', 'eq_ref', 'ref') THEN '✅ 优秀'
      WHEN type = 'range' THEN '⚠️ 良好'
      WHEN type IN ('index', 'ALL') THEN '❌ 需优化'
      ELSE '❓ 未知'
    END,
    ' | ',
    '访问类型: ', type,
    ' | ',
    '扫描行数: ', rows,
    ' | ',
    'Extra: ', IFNULL(Extra, '无')
  ) AS health_check
FROM (
  -- 替换为你的EXPLAIN结果
  SELECT * FROM (
    EXPLAIN SELECT * FROM users WHERE age = 25
  ) AS explain_result
) AS t;

7.2 常见陷阱与规避方法

陷阱1:过度依赖EXPLAIN的预估

问题:EXPLAIN的rows字段是优化器预估,可能与实际相差巨大。

sql 复制代码
-- 示例:预估偏差
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: rows=1000 (预估)

EXPLAIN ANALYZE SELECT * FROM users WHERE age = 25;
-- Output: actual rows=9876 (实际相差10倍!)

原因

  • 统计信息过期(ANALYZE TABLE未及时执行)
  • 数据分布不均匀
  • 相关性未考虑(多列条件的相关性)

解决方案

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

-- 使用EXPLAIN ANALYZE获取真实数据
EXPLAIN ANALYZE SELECT ...;

-- 启用直方图(MySQL 8.0+)
ANALYZE TABLE users UPDATE HISTOGRAM ON age, status WITH 100 BUCKETS;
陷阱2:忽视filtered字段

问题:只关注rows,忽略filtered百分比。

sql 复制代码
-- 示例:高rows但高过滤
EXPLAIN SELECT * FROM users WHERE age > 20 AND status = 1;
-- rows=100000, filtered=10.00
-- 实际有效行数 = 100000 × 10% = 10000

最佳实践

sql 复制代码
-- 计算实际有效行数
SELECT 
  table,
  rows AS 预估扫描行数,
  filtered AS 过滤百分比,
  ROUND(rows * filtered / 100, 0) AS 实际有效行数
FROM EXPLAIN SELECT * FROM users WHERE age > 20 AND status = 1;
陷阱3:忽略索引选择性

问题:添加了索引但未考虑选择性。

sql 复制代码
-- 低选择性索引( gender 只有2个值)
ALTER TABLE users ADD INDEX idx_gender (gender);

-- EXPLAIN显示:type=ref, rows=500000(几乎全表)

索引选择性计算

sql 复制代码
-- 计算索引选择性(越接近1越好)
SELECT 
  COUNT(DISTINCT age) / COUNT(*) AS age_selectivity,
  COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM users;

-- 输出:
-- age_selectivity: 0.95 (高选择性,适合建索引)
-- gender_selectivity: 0.02 (低选择性,不适合建索引)

最佳实践

  • ✅ 选择性 > 0.1:适合建索引
  • ⚠️ 选择性 0.01-0.1:考虑复合索引
  • ❌ 选择性 < 0.01:不适合建索引
陷阱4:ORDER BY误用导致filesort

问题:索引列顺序与ORDER BY不一致。

sql 复制代码
-- 索引:idx_age_status (age, status)
-- 查询:
SELECT * FROM users 
WHERE age = 25 
ORDER BY status;  -- ✅ 无filesort

SELECT * FROM users 
WHERE status = 1 
ORDER BY age;     -- ❌ 出现filesort

最佳实践

sql 复制代码
-- ORDER BY遵循索引最左前缀原则
-- ✅ 正确:ORDER BY age, status
-- ✅ 正确:ORDER BY age(部分前缀)
-- ❌ 错误:ORDER BY status, age(跳过前缀)
-- ❌ 错误:ORDER BY age DESC, status ASC(方向不一致)

7.3 EXPLAIN在不同MySQL版本的差异

特性 MySQL 5.7 MySQL 8.0 差异说明
EXPLAIN ANALYZE ❌ 不支持 ✅ 支持 8.0提供真实执行统计
直方图 ❌ 不支持 ✅ 支持 8.0统计信息更精准
不可见索引 ❌ 不支持 ✅ 支持 8.0可测试索引效果
窗口函数 ❌ 不支持 ✅ 支持 8.0新增执行计划类型
** descending index** ❌ 不支持 ✅ 支持 8.0支持降序索引
Skip Scan ❌ 不支持 ✅ 支持 8.0新增扫描方式

版本升级建议

sql 复制代码
-- 检查当前版本
SELECT VERSION();

-- 利用8.0新特性优化查询
-- 示例:直方图辅助优化器
ANALYZE TABLE users UPDATE HISTOGRAM ON age, status WITH 100 BUCKETS;

-- 示例:不可见索引测试
ALTER TABLE users ALTER INDEX idx_age INVISIBLE;
-- 测试性能
ALTER TABLE users ALTER INDEX idx_age VISIBLE;

8. 总结与展望

8.1 核心要点回顾

本文系统性地解析了MySQL EXPLAIN执行计划的方方面面,以下是核心知识图谱:
EXPLAIN执行计划
基础
EXPLAIN
EXPLAIN ANALYZE
FORMAT=JSON/TREE
核心字段
id
select_type
type⭐
key/key_len
rows/filtered
Extra⭐
优化方向
索引优化
查询改写
结构调整
高级技巧
成本模型
源码分析
版本差异

8.2 执行计划分析思维模型

建立科学的SQL优化思维模型:
ALL/index
range/ref
filesort
temporary
Using where


发现慢查询
EXPLAIN基础分析
type字段检查
索引优化
进一步分析
添加/调整索引
Extra检查
优化ORDER BY
优化GROUP BY
检查覆盖索引
EXPLAIN验证
满意?
EXPLAIN ANALYZE
完成
实际执行分析
优化器成本调整

8.3 进阶学习路径

  1. 源码级深入

    • 阅读sql/sql_optimizer.cc(优化器主逻辑)
    • 研究sql/sql_executor.cc(执行器实现)
    • 理解成本模型(sql/opt_costmodel.cc
  2. 实战能力提升

    • 收集100+个慢查询案例
    • 建立执行计划分析checklist
    • 总结各业务场景的优化模式
  3. 工具生态

    • MySQL Workbench可视化执行计划
    • pt-query-digest(Percona Toolkit)
    • MySQL Enterprise Monitor

8.4 未来展望

MySQL执行计划技术仍在持续演进:

  • AI驱动优化:MySQL 9.0引入机器学习优化器
  • 实时自适应:根据实际执行动态调整计划
  • 云原生优化:针对分布式架构的执行计划改进

📚 参考资料

  1. MySQL官方文档

  2. 源码阅读

    • MySQL 8.0.35 Source Code: sql/sql_explain.cc
    • MySQL 8.0.35 Source Code: sql/sql_optimizer.cc
  3. 推荐文章

  4. 工具


🎯 练习题

练习1:分析以下SQL的执行计划并优化

sql 复制代码
SELECT * FROM orders o 
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1 
  AND u.age > 20
ORDER BY o.create_time DESC 
LIMIT 100;

练习2:为以下查询设计最优索引

sql 复制代码
SELECT COUNT(*) 
FROM users 
WHERE age BETWEEN 20 AND 30 
  AND status = 1;

练习3:解释为什么会出现Using temporary并优化

sql 复制代码
SELECT age, COUNT(*) 
FROM users 
GROUP BY age 
ORDER BY MAX(create_time);

恭喜你完成了MySQL执行计划的深度学习之旅! 🎉

掌握了EXPLAIN,你就掌握了SQL性能优化的金钥匙。继续实践,持续精进!


标签[MySQL] [执行计划] [EXPLAIN] [查询优化] [数据库调优] [性能分析] [源码解析]

版权声明 :本文为原创技术文章,转载请注明出处。
作者简介:[你的名字] - 数据库性能优化专家,专注于MySQL内核研究与实战优化。

相关推荐
FirstFrost --sy2 小时前
MySql 内外连接
android·数据库·mysql
卤炖阑尾炎2 小时前
MySQL 主从复制与读写分离:从原理到实战全解析
mysql·adb
ego.iblacat2 小时前
MySQL 高可用
数据库·mysql·adb
bearpping3 小时前
关于Mysql 中 Row size too large (> 8126) 错误的解决和理解
数据库·mysql
爱丽_3 小时前
事务隔离级别与一致性:从现象到实现(MVCC 与当前读)
数据库·mysql
X-⃢_⃢-X4 小时前
四、索引的创建与设计原则
数据库·mysql
满天星83035775 小时前
【MySQL】表的基本查询(上)
linux·服务器·数据库·mysql
川石课堂软件测试5 小时前
涨薪技术|Prometheus使用Recoding Rules优化性能
功能测试·测试工具·jmeter·mysql·面试·单元测试·prometheus
主角1 75 小时前
MySQL高可用集群
数据库·mysql