跳至正文
来两杯美式
返回

MySQL 知识系列(七):SQL 调优(一)—— EXPLAIN 执行计划

By 来两杯美式
发布于

本文是 MySQL 知识系列的第七篇——SQL 调优专题。前面六篇我们已经掌握了 MySQL 的架构、索引、锁、日志、事务等基础知识。SQL 调优是这些知识点的综合应用——为什么 EXPLAIN 看 type 是 ALL 时 SQL 很慢?为什么 Extra 出现 Using filesort 就要优化?这一篇全部讲清楚。

为什么要先懂 EXPLAIN

EXPLAIN SELECT ... 是 MySQL 提供的”SQL 执行计划分析工具”——它不真正执行 SQL,而是告诉你”MySQL 打算怎么执行这条 SQL”:

调优的第一步永远是 EXPLAIN。不先看执行计划,凭直觉改 SQL 经常是”瞎改”。

EXPLAIN 输出字段详解

EXPLAIN 输出 12 列,重点关注以下 7 列:

字段含义关注度
table这一行对应哪张表低(多表 JOIN 时看顺序)
type连接类型(访问类型)⭐⭐⭐ 最高
possible_keys可能用到的索引
key实际用到的索引⭐⭐
key_len使用的索引长度
ref索引的哪一列被使用
rows估算扫描行数⭐⭐
Extra额外信息(关键判断)⭐⭐⭐ 最高

type 字段(连接类型)

从最好到最差

system  >  const  >  eq_ref  >  ref  >  range  >  index  >  ALL
类型含义性能
system表只有一行数据(系统表)最优
const主键/唯一索引的等值查询,最多匹配 1 行极优
eq_refJOIN 时,对前表的每行,主键/唯一索引等值匹配本表 1 行极优
ref非唯一索引/联合索引的前缀等值匹配
range索引范围扫描(><INBETWEEN
index索引扫描(不走数据行,但要走所有索引条目)较差
ALL扫描

💡 实战目标

  • 至少达到 range 级别
  • 索引设计良好时可达 ref / eq_ref
  • 出现 indexALL 必须优化

possible_keys / key / key_len / ref

EXPLAIN SELECT * FROM user WHERE name = 'a' AND age = 10;
-- 强制使用/忽略某个索引
SELECT * FROM user FORCE INDEX (idx_name) WHERE name = 'a';
SELECT * FROM user IGNORE INDEX (idx_name) WHERE name = 'a';

rows

MySQL 估算需要扫描的行数。估算值,不是实际值——基于索引统计信息(采样估算)。

⚠️ rows 只是个参考,不应作为”准确行数”。

Extra 字段(最容易踩坑的诊断信息)

Extra 包含关键诊断信息,出现以下关键字通常意味着需要优化

关键字含义处理
Using filesortMySQL 做了额外的排序(不能走索引排序)必须优化
Using temporaryMySQL 创建了临时表(通常出现在 GROUP BY / DISTINCT)必须优化
Using where用 WHERE 过滤数据(一般没问题)
Using index覆盖索引(无需回表,性能好)
Using index condition索引下推(ICP),InnoDB 5.6+ 特性
Using join bufferJOIN 用到了 join buffer(一般是 ALL 扫描)需优化
Impossible WHEREWHERE 永远为 false逻辑问题
Distinct找到第一个匹配后停止
Not existsLEFT JOIN 优化,找到匹配就停

💡 “需要优化”的信号

  • Using filesort —— ORDER BY 没走索引
  • Using temporary —— GROUP BY / DISTINCT 没走索引
  • Using join buffer —— 关联字段没索引
  • Range checked for each record —— 关联字段没合适索引,每次都临时选

实战:看懂 EXPLAIN 输出

下面是一个典型的”需要优化”案例:

索引基数 Cardinality 示例(store_id 基数仅 2,应作为联合索引第二列)

红框标出的是 Cardinality(基数)——索引中唯一值的数量估值。基数越高,索引区分度越好:

问题store_id 单独建在联合索引首列,区分度太差,优化器可能放弃该索引。优化建议:把高基数列 film_id 放到联合索引首列。

-- 查询索引基数
SHOW INDEX FROM table_name;

-- 重新统计基数(基数不准确时)
ANALYZE TABLE table_name;

索引排序与 ORDER BY 优化

执行流程

SELECT * FROM table_name WHERE id > 100 ORDER BY column1;

执行步骤:

  1. 按 WHERE 条件查数据
  2. 将结果集放入 sort_buffer排序专用缓存
  3. sort_buffer 中按 ORDER BY 排序
  4. 如有需要,回表二次查询补充完整数据

⚠️ 中间结果集忽略索引的有序性——所以这一步会触发 Using filesort

用索引扫描来优化排序

使用索引扫描来优化 ORDER BY 的 3 个条件

ORDER BY 走索引扫描要同时满足 3 个条件:

  1. 索引列顺序与 ORDER BY 子句完全一致
  2. 索引列方向(升序/降序)与 ORDER BY 完全一致
  3. ORDER BY 字段全部在 JOIN 的第一张表
-- 假设联合索引 (a, b, c)
SELECT * FROM t WHERE a = 1 ORDER BY b, c;   -- ✓ 走索引排序
SELECT * FROM t WHERE a = 1 ORDER BY c, b;   -- ✗ 走 filesort

sort_buffer 配置

SHOW VARIABLES LIKE 'sort_buffer_size';      -- 默认 256K

💡 优化思路:让 WHERE 和 ORDER BY 走同一个索引,走覆盖索引(避免回表)。

LIMIT 优化(深分页)

SELECT * FROM table_name ORDER BY column1 LIMIT 900, 10;

问题

MySQL 先取出前 900 条 + 10 条,再丢掉前 900 条——深分页时性能极差

优化思路

先走覆盖索引拿到主键 ID,再回表

-- 子查询方式(MySQL 5.7+)
SELECT *
FROM table_name t
JOIN (
    SELECT id FROM table_name
    ORDER BY column1
    LIMIT 900, 10
) AS tmp ON t.id = tmp.id;

-- 书签记录方式(更优,记住上次查询的最大 ID)
SELECT * FROM table_name
WHERE column1 > 'last_seen_value'
ORDER BY column1
LIMIT 10;

松散索引扫描(Loose Index Scan)

这是 MySQL 8.0 的重要新特性。

场景:联合索引 (column1, column2),单独用 column2 过滤。

虽然效率不如最左前缀快,但优化了原本全表扫描的查询


下一篇(篇八)会讲”SQL 调优(二)“:索引设计原则、最左前缀底层原理、覆盖索引实战、慢查询日志分析、pt-query-digest 工具、COUNT 函数选型——把 SQL 调优的”调优动作”全部讲透。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
MySQL
第 7 / 9 篇
查看系列全部文章
  1. 01.MySQL 知识系列(一):整体架构与存储引擎
  2. 02.MySQL 知识系列(二):索引结构与查询原理
  3. 03.MySQL 知识系列(三):锁机制与并发控制
  4. 04.MySQL 知识系列(四):日志系统:redo log、undo log、binlog
  5. 05.MySQL 知识系列(五):隔离级别、Spring 事务与查询流程
  6. 06.MySQL 知识系列(六):SQL 注入、版本特性与容量评估
  7. 07.MySQL 知识系列(七):SQL 调优(一)—— EXPLAIN 执行计划
  8. 08.MySQL 知识系列(八):SQL 调优(二)—— 索引设计与调优工具
  9. 09.MySQL 知识系列(九):CASE 语句、乐观锁与悲观锁实战

上一篇
MySQL 知识系列(八):SQL 调优(二)—— 索引设计与调优工具
下一篇
Redis 知识系列(四):集群与缓存架构