EXPLAIN解读

EXPLAIN 是慢查询优化最重要的工具。你在 SQL 前面加 EXPLAIN,MySQL 不会真正执行这条 SQL,而是告诉你它”打算怎么执行”——走不走索引、扫多少行、用什么方式访问表。

1.0 先说清楚:EXPLAIN 返回什么?

 
EXPLAIN SELECT * FROM users WHERE name = '张三';
 
 
输出大概长这样(不同 MySQL 版本列数略有差异):
 
 
 
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
 
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
 
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
 
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 5000 | Using where |
 
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
 
 
 
下面逐列拆解。不用死记每一列的每个可能值——记住"type 列 + Extra 列"就覆盖了 80% 的问题。
 

1.1 id — 执行顺序

 
id 是 SELECT 的序号。简单查询就一个 1,不用管。
 
 
 
多个表 JOIN 时,id 相同的从上往下执行;id 不同的越大越先执行。
 
 
 
你只要记住:出现子查询时,id 越大的越先跑。不涉及也没关系,不重要。
 

1.2 select_type — 查询类型

 
常见值(碰到哪个认识就行):
 
 
 
  SIMPLE       → 普通查询,没有子查询、没有 UNION
 
  PRIMARY      → 最外层的 SELECT(有子查询时才有)
 
  SUBQUERY     → 子查询里的 SELECT
 
  DERIVED      → FROM 子句里的子查询(MySQL 叫"派生表")
 
  UNION        → UNION 后面的 SELECT
 
 
 
不用全背,认识 SIMPLE 和 SUBQUERY 就够了。
 

1.3 table — 查哪张表

 
就是表名。没东西可说。
 

1.4 🔴 type — 访问方式(最重要的一列!它决定你的 SQL 快不快)

 
type 的值从最好到最差排列。优化 SQL 的目标就是把 type 往左边推。
 
 
 
  system > const > eq_ref > ref > range > index > ALL
 
  ═══════════════════════        ══════════════════════
 
  快到起飞 ✅                     能过                    💀 全表扫描,死刑
 
 
 
逐一看:
 
 
 
──────────────────────────────────────────────────────────────
 
system(最佳——几乎看不见)
 
──────────────────────────────────────────────────────────────
 
  表里只有一行数据(系统表、配置表),查它 = 直接拿。
 
  你在真实项目中看不到这个值——100 万行的表不可能走 system。
 
 
 
──────────────────────────────────────────────────────────────
 
const(顶级快——通过主键或唯一索引查一行)
 
──────────────────────────────────────────────────────────────
 
  条件里有主键 = 或唯一索引 = → MySQL 知道最多返回一行 → 当常量处理。
 
 
 
  EXPLAIN SELECT * FROM users WHERE id = 5;
 
  → type: const ← "我直接用主键定位,不用扫"
 
 
 
  类似你去图书馆,知道书在"3楼A区5排3座" → 直接走 → 不等不找。
 
 
 
──────────────────────────────────────────────────────────────
 
eq_ref(JOIN 时通过主键关联——也是顶级的)
 
──────────────────────────────────────────────────────────────
 
  两表 JOIN,驱动表的每一行,在被驱动表里通过主键或唯一索引找到唯一匹配行。
 
 
 
  EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
 
  → users 表走 eq_ref(用主键 id 匹配)
 
 
 
──────────────────────────────────────────────────────────────
 
ref(好——普通索引等值查找)
 
──────────────────────────────────────────────────────────────
 
  用非唯一索引查数据,可能返回多行。这是你日常开发最常见的"正常快"。
 
 
 
  EXPLAIN SELECT * FROM users WHERE name = '张三';
 
  → 如果 name 列有普通索引 → type: ref ✅
 
 
 
  ref 和 const 的区别:
 
    const = 唯一索引找一行,最多 1 行(主键 = 或 unique 索引 =)
 
    ref   = 非唯一普通索引,可能返回 N 行(如 name = '张三' 可能有 3 个人叫张三)
 
 
 
──────────────────────────────────────────────────────────────
 
range(还行——索引范围扫描)
 
──────────────────────────────────────────────────────────────
 
  WHERE 里用了 >、<、>=、<=、BETWEEN、IN、LIKE '张%' 等范围条件。
 
  走索引找范围的起止位置,中间全取——不会扫整个索引。
 
 
 
  EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-01';
 
  → type: range ✅
 
 
 
  EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
 
  → type: range ✅
 
 
 
  EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3);
 
  → type: range ✅
 
 
 
  ⚠️ IN 是 range!即使只有 3 个值也是 range——多个等值 = 范围。
 
 
 
──────────────────────────────────────────────────────────────
 
index(⚠️ 假索引——扫了整个索引树,没扫表但也不是好事)
 
──────────────────────────────────────────────────────────────
 
  看起来走了索引?不——它遍历了整个索引树。
 
 
 
  和 ALL(全表扫描)的区别:
 
    ALL  → 扫完整张表的数据页
 
    index → 扫完整棵索引树
 
    索引树比数据表小(只有索引列),所以 index 比 ALL 快一点——但仍然是"全扫"。
 
 
 
  什么时候出现?
 
    ① 查询的列全在索引里(满足覆盖索引),但没有 WHERE 条件 → 只能扫整个索引
 
    ② 需要排序,ORDER BY 的列有索引 → 直接读索引就不用再排序了
 
 
 
  EXPLAIN SELECT name FROM users ORDER BY name;
 
  → 如果 name 有索引,type 可能是 index
 
  → 虽然没有 WHERE 条件要扫整个索引,但至少只扫了 name 列而不是整行数据
 
 
 
  ⚠️ 如果你看到 type=index 但 rows 有几百万 → 还是慢!别被"用了索引"骗了。
 
 
 
──────────────────────────────────────────────────────────────
 
ALL(💀 全表扫描——SQL 届的死刑判决书)
 
──────────────────────────────────────────────────────────────
 
  MySQL 一行一行从头翻到尾——1000 万行就翻 1000 万次。
 
 
 
  常见原因:
 
    ① WHERE 条件列没建索引
 
    ② 索引失效了(后面详解)
 
    ③ LIKE '%张三' ← 前置模糊,索引用不了
 
 
 
  EXPLAIN SELECT * FROM users WHERE nickname = '小明';
 
  → nickname 没索引 → type: ALL 💀
 
  → rows: 5000000 ← 500 万行!全翻一遍!
 
 
 
 
 
一张图记一辈子:
 
 
 
  动手画这个——
 
 
 
  system  → 几乎不存在
 
  const   → 主键/唯一索引查一行(秒杀)
 
  eq_ref  → JOIN 时主键关联(秒杀)
 
  ref     → 普通索引等值查(快,日常常态)
 
  range   → 索引范围扫描(还行,IN / BETWEEN / > / <)
 
  index   → 扫了整个索引树(⚠️ 假积极——实际在偷懒)
 
  ALL     → 全表扫描(💀 死刑,必须优化)
 

1.5 possible_keys 和 key — “可以选”和”选了谁”

 
possible_keys → MySQL 认为"理论上能用的索引有哪些"
 
key           → MySQL 最终决定用哪个
 
 
 
两者可能不一样!比如:
 
 
 
  EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 22;
 
  → possible_keys: idx_name, idx_age, idx_name_age
 
  → key: idx_name_age  ← 三个里选了最优的联合索引
 
 
 
key 为 NULL → 💀 一个索引都没用上 → 回去看你的 WHERE 条件列有没有索引 / 索引有没有失效
 

1.6 key_len — 索引用了多长

 
key_len 告诉你"这个复合索引用了前几列"。
 
 
 
假设有一个复合索引 idx_name_age(name, age)——两列联合索引,name 在前 age 在后。
 
 
 
  EXPLAIN SELECT * FROM users WHERE name = '张三';
 
  → key_len: 某个值 ← 只用了 name 这一列,age 没用到
 
 
 
  EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 22;
 
  → key_len: 更大的值 ← name 和 age 两列全用上了
 
 
 
实用结论:key_len 越大 = 复合索引用到的列越多 = 过滤越精准 = 越快。
 
具体字节数通常不需要手算,知道大→好就行。
 

1.7 ref — 索引用谁和谁比

 
ref 列告诉你:索引列和"什么东西"做的等值匹配。
 
 
 
常见值:
 
  const   → 和常量比:WHERE name = '张三' → ref: const
 
  func    → 和函数结果比:WHERE name = LOWER('张三') → ref: func
 
  列名     → JOIN 时和另一个表的列比:ref: shop_db.users.id
 
 
 
这一列基本用不到,知道就行。
 

1.8 🔴 rows — 预估扫描行数

 
MySQL 说"我觉得我要扫这么多行才能拿到结果"。
 
 
 
这个数字越小越好!它是估算值,不准但够用。
 
 
 
  1000 万行的表,rows = 3    → 走主键,只扫 3 行 ✅
 
  1000 万行的表,rows = 500 万 → 全表扫描 💀
 
 
 
rows 远大于最终返回行数 → 索引有问题。
 
rows 接近最终返回行数 → 索引用得不错。
 

1.9 filtered — 扫描后还剩多少

 
MySQL 5.7+ 才有。rows × filtered% = 最终剩余行数。
 
 
 
  rows=1000, filtered=10.00 → 扫 1000 行,条件过滤后剩 100 行
 
  rows=1000, filtered=100.00 → 扫 1000 行,全符合条件,无需再过滤
 
 
 
filtered 越低 = 索引过滤性越差 = 扫了很多用不上的行。
 

1.10 🔴 Extra — 附加信息(第二重要的列!)

 
type 告诉你"怎么找",Extra 告诉你"找的时候还干了什么"。
 
 
 
═══════════════════════════════════════════════════════════════
 
✅ 好消息(见到就放心):
 
═══════════════════════════════════════════════════════════════
 
 
 
  Using index(覆盖索引):
 
    → 所有需要的列全在索引里 → 不用回表 → 超级快!
 
 
 
    假设索引 idx(name, age) 建在 users 表。
 
    SELECT name, age FROM users WHERE name = '张三';
 
    → 查 name 和 age,这两列全在索引里 → 只读索引树就够了,不回表
 
    → Extra: Using index ✅
 
 
 
  Using where:
 
    → 正常——用 WHERE 条件过滤。只要不是 Using where + Using filesort 就没事。
 
 
 
  Using index condition:
 
    → ICP(索引条件下推)——MySQL 5.6+ 优化,利用索引先过滤再回表。
 
    → 好事,不用管。
 
 
 
 
 
═══════════════════════════════════════════════════════════════
 
⚠️ 警告信号(见到得想想能不能优化):
 
═══════════════════════════════════════════════════════════════
 
 
 
  Using filesort:
 
    → 文件排序——ORDER BY 的列没走索引,MySQL 额外开了一块空间排序。
 
    → 数据少没事,数据多了(几十万行)→ 慢 + 吃内存。
 
 
 
    SELECT * FROM users ORDER BY created_at;
 
    → created_at 没索引 → 全扫出来 → 额外排序 → Extra: Using filesort
 
 
 
    解法:在 ORDER BY 的列上建索引。
 
 
 
  Using temporary:
 
    → 用了临时表——GROUP BY / DISTINCT / UNION 时,MySQL 在内存/磁盘建了张临时表。
 
    → 比 filesort 更严重——临时表可能被刷到磁盘,非常慢!
 
 
 
    SELECT category, COUNT(*) FROM products GROUP BY category;
 
    → category 没索引 → MySQL 建临时表 → 把数据扔进去 → 分组计数
 
    → Extra: Using temporary; Using filesort
 
 
 
    解法:GROUP BY 的列建索引。
 
 
 
 
 
═══════════════════════════════════════════════════════════════
 
💀 死刑信号:
 
═══════════════════════════════════════════════════════════════
 
 
 
  Using filesort + Using temporary 同时出现:
 
    → 这条 SQL 没救了,必须改索引或改写 SQL。
 
    出现这种情况说明 MySQL 要额外操作两次——先建临时表,再排序。
 

一图流(Mermaid)


flowchart TD

    A[EXPLAIN 诊断慢SQL] --> B{看两列就够}

    B --> C[type 列: 访问方式 决定快不快]

    B --> D[Extra 列: 找时还干了啥]

    C --> E[system>const>eq_ref>ref>range>index>ALL<br/>目标: 往左推, 躲开 ALL 💀]

    D --> F{信号}

    F -->|Using index| G[覆盖索引 ✅ 不用回表]

    F -->|Using filesort/temporary| H[⚠️ 排序/临时表 建索引]

    F -->|filesort+temporary 同现| I[💀 必须改 SQL]

速记卡(面试闪卡)

Q1:一句话讲清「EXPLAIN解读」到底是什么?

A:EXPLAIN 是 MySQL 的 SQL 体检报告:在 SQL 前加它,不真正执行,只告诉你打算怎么查——走没走索引、扫多少行。

Q2:type 列怎么排 —— 怎么理解?

A:像看病轻重:system > const > eq_ref > ref > range > index > ALL。const 是主键查一行(秒杀),ref 是普通索引等值(日常快),range 是范围(IN/BETWEEN),index 是扫整棵索引树(假积极),ALL 是全表扫描(死刑)。目标把 type 往左推、躲开 ALL。

Q3:Extra 列看什么信号 —— 怎么理解?

A:像医生的附注:Using index(覆盖索引,好事,不用回表);Using filesort / Using temporary(排序/临时表,说明 ORDER BY、GROUP BY 没走索引,得建索引);filesort + temporary 同时出现=这条 SQL 没救了,必须改。

Q4:key 为 NULL 说明啥 —— 怎么理解?

A:像没带钥匙:possible_keys 是”理论能用的索引”,key 是”最终选的”。key 为 NULL 说明一个索引都没用上——回去看 WHERE 条件列有没有建索引,或者索引是不是失效了(如 LIKE ‘%张三’ 前置模糊)。

Q5:rows 和 filtered 怎么看 —— 怎么理解?

A:像预估工作量:rows 是 MySQL 估的扫描行数,越小越好(1000 万表 rows=3 走主键,rows=500 万就是全表扫)。filtered 是扫完还剩下的百分比,越低说明索引过滤性越差、扫了很多用不上的行。

Q6:核心速记主线有哪些?

  • type 列:访问方式定快慢,目标往左推躲开 ALL(全表扫描)

  • Extra 列:Using index 最香,filesort+temporary 同现必改

  • key 为 NULL:没用上索引,查 WHERE 列或索引失效

  • rows/filtered:扫描行数越小越好,filtered 低=过滤差

口诀

A:EXPLAIN 看 type,Extra 也别漏;

type 往左推,躲开 ALL 这死囚。

覆盖索引香,回表不用愁;

filesort 加临时,必改莫停留。

相关链接