本文是 MySQL 知识系列的第七篇——SQL 调优专题。前面六篇我们已经掌握了 MySQL 的架构、索引、锁、日志、事务等基础知识。SQL 调优是这些知识点的综合应用——为什么 EXPLAIN 看 type 是
ALL时 SQL 很慢?为什么 Extra 出现Using filesort就要优化?这一篇全部讲清楚。
为什么要先懂 EXPLAIN
EXPLAIN SELECT ... 是 MySQL 提供的”SQL 执行计划分析工具”——它不真正执行 SQL,而是告诉你”MySQL 打算怎么执行这条 SQL”:
- 走哪个索引(或者不走)
- 估算要扫描多少行
- 是否会做排序、临时表等额外操作
- 多表 JOIN 的连接顺序
调优的第一步永远是 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_ref | JOIN 时,对前表的每行,主键/唯一索引等值匹配本表 1 行 | 极优 |
| ref | 非唯一索引/联合索引的前缀等值匹配 | 优 |
| range | 索引范围扫描(>、<、IN、BETWEEN) | 良 |
| index | 全索引扫描(不走数据行,但要走所有索引条目) | 较差 |
| ALL | 全表扫描 | 差 |
💡 实战目标:
- 至少达到 range 级别
- 索引设计良好时可达 ref / eq_ref
- 出现 index 或 ALL 必须优化
possible_keys / key / key_len / ref
EXPLAIN SELECT * FROM user WHERE name = 'a' AND age = 10;
- possible_keys:MySQL 认为可能用到的索引(基于统计信息,不一定准)
- key:MySQL 实际选择的索引(
NULL表示没走索引) - key_len:使用的索引长度,在不损失精度的情况下,越短越好(单位字节)
- ref:哪些列或常量被用于索引查找
-- 强制使用/忽略某个索引
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 filesort | MySQL 做了额外的排序(不能走索引排序) | 必须优化 |
Using temporary | MySQL 创建了临时表(通常出现在 GROUP BY / DISTINCT) | 必须优化 |
Using where | 用 WHERE 过滤数据(一般没问题) | — |
Using index | 覆盖索引(无需回表,性能好) | ✓ |
Using index condition | 索引下推(ICP),InnoDB 5.6+ 特性 | ✓ |
Using join buffer | JOIN 用到了 join buffer(一般是 ALL 扫描) | 需优化 |
Impossible WHERE | WHERE 永远为 false | 逻辑问题 |
Distinct | 找到第一个匹配后停止 | — |
Not exists | LEFT JOIN 优化,找到匹配就停 | — |
💡 “需要优化”的信号:
Using filesort—— ORDER BY 没走索引Using temporary—— GROUP BY / DISTINCT 没走索引Using join buffer—— 关联字段没索引Range checked for each record—— 关联字段没合适索引,每次都临时选
实战:看懂 EXPLAIN 输出
下面是一个典型的”需要优化”案例:

红框标出的是 Cardinality(基数)——索引中唯一值的数量估值。基数越高,索引区分度越好:
PRIMARY (inventory_id):基数 4581idx_fk_film_id (film_id):基数 958idx_store_id_film_id第 1 列store_id:基数 2(区分度极差)idx_store_id_film_id第 2 列film_id:基数 1521
问题: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;
执行步骤:
- 按 WHERE 条件查数据
- 将结果集放入
sort_buffer(排序专用缓存) - 在
sort_buffer中按ORDER BY排序 - 如有需要,回表二次查询补充完整数据
⚠️ 中间结果集忽略索引的有序性——所以这一步会触发
Using filesort。
用索引扫描来优化排序

ORDER BY 走索引扫描要同时满足 3 个条件:
- 索引列顺序与
ORDER BY子句完全一致 - 索引列方向(升序/降序)与
ORDER BY完全一致 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
sort_buffer较小时放内存- 超过阈值时放磁盘(性能急剧下降)
💡 优化思路:让 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 过滤。
- MySQL 8.0 之前:单独用
column2不能用此联合索引(违反最左前缀) - MySQL 8.0 之后:可以使用”松散索引扫描”,打破最左前缀原则
虽然效率不如最左前缀快,但优化了原本全表扫描的查询。
下一篇(篇八)会讲”SQL 调优(二)“:索引设计原则、最左前缀底层原理、覆盖索引实战、慢查询日志分析、pt-query-digest 工具、COUNT 函数选型——把 SQL 调优的”调优动作”全部讲透。