MySQL 8.0 官方文档解析:理解查询执行计划(10.8)
1、 核心概念:什么是查询执行计划(EXPLAIN Plan)
在 MySQL 中,查询执行计划(Query Execution Plan)是指优化器(Optimizer)为了高效执行一条 SQL 查询,在综合评估了表结构、列定义、索引细节以及 WHERE 子句条件后,所选择的一组最优操作集合。
- 工作原理:优化器会在多种可用的查找技术中进行成本计算与抉择。例如,对于超大表的查询,优化器会尝试避免全表扫描;对于包含多个连接(JOIN)的查询,优化器会评估不同的表连接顺序,而不是穷举所有组合。
- EXPLAIN Plan:优化器最终选择的这套最高效的操作集合,即为执行计划,官方也称之为
EXPLAIN计划。
2、 核心目标:为什么要使用 EXPLAIN
EXPLAIN 语句的核心作用在于透视 SQL 的底层执行逻辑。开发者查阅执行计划的主要目标包括:
- 识别性能瓶颈:准确找出查询计划中指示需要优化的环节(如索引失效、全表扫描、文件排序等)。
- 指导优化动作:结合对 SQL 语法和索引原理的掌握,识别出低效的执行方案,并据此采取行动(如重构 SQL、调整或新增索引)来改进执行计划。
3、 知识图谱:本节结构概览
官方文档在“理解查询执行计划”大类下,按进阶逻辑划分为五个子模块:
- 10.8.1 用 EXPLAIN 优化查询:基础用法及适用场景。
- 10.8.2 EXPLAIN 输出格式:传统表格格式的字段解析(核心重点)。
- 10.8.3 扩展 EXPLAIN 输出格式:如 JSON 格式输出,提供更详尽的成本信息。
- 10.8.4 获取命名连接的执行计划信息:针对特定连接(Connection ID)的追踪。
- 10.8.5 查询性能估计:使用
EXPLAIN ANALYZE进行真实执行耗时的评估。
使用explain 优化查询
1、 核心功能与适用范围
EXPLAIN 语句是 MySQL 提供的用于获取 SQL 语句执行计划的核心工具。它用于解析 MySQL 优化器将如何执行该语句,包括表的连接方式及连接顺序等关键信息。
- 支持的语句类型:
EXPLAIN可与SELECT,DELETE,INSERT,REPLACE以及UPDATE语句结合使用。 - 针对 SELECT 的扩展:对于
SELECT语句,EXPLAIN还会生成额外的执行计划信息,可通过SHOW WARNINGS命令查看(详见 10.8.3 扩展输出格式)。 - 分区表支持:在检查涉及分区表的查询时,
EXPLAIN同样能提供相关执行信息(详见 26.3.5 节)。
2、 核心应用场景
- 索引优化指导:通过
EXPLAIN的输出,可精准定位查询的瓶颈,从而判断应该在何处添加索引,使语句能够通过索引查找数据,显著提升执行速度。 - 表连接顺序优化:可用于检查优化器(Optimizer)是否以最优顺序连接了多张表。虽然可以通过
STRAIGHT_JOIN提示词强制优化器按SELECT语句中表出现的顺序进行连接,但需注意,这可能会禁用半连接转换,从而阻止索引的使用(详见 15.2.13 和 10.2.2.1 节)。 - 诊断索引失效:若出现预期内的索引未被使用的情况,应运行
ANALYZE TABLE命令。该命令用于更新表的统计信息(如键的基数等),统计信息的准确性直接影响优化器的决策(详见 15.7.3.1 节)。
3、 输出格式配置 (FORMAT 选项)
EXPLAIN 允许用户选择不同的输出格式:
- 默认格式:传统表格格式(TRADITIONAL),直观易读。
- JSON 格式:通过
FORMAT=JSON选项输出。提供比传统表格更详细、结构化的执行计划信息,尤其适用于复杂查询的深度分析。
4、 补充说明与高级特性
- 获取命名连接的执行计划:结合
FOR CONNECTION connection_id语法,可以获取特定命名连接中正在执行的语句的执行计划信息(详见 10.8.4 节)。 - 优化器追踪(Optimizer Trace):优化器追踪可提供
EXPLAIN之外的补充信息。但需注意,追踪输出的格式和内容可能因 MySQL 版本不同而存在差异(详见 10.15 节)。 - 特殊用法(注释):
EXPLAIN tbl_name是DESCRIBE tbl_name或SHOW COLUMNS FROM tbl_name的同义词,也可用于获取表的列结构信息(详见 15.8.1 节)。
explain 输出格式概述
1. 核心机制与执行顺序
EXPLAIN 语句的核心作用是解析 MySQL 优化器将如何执行一条语句(适用于 SELECT, DELETE, INSERT, REPLACE, UPDATE)。
- 输出特征:
EXPLAIN会为语句中涉及到的每一张表返回一行信息。 - 读取顺序判定:返回结果的行顺序,代表了 MySQL 在执行查询时读取这些表的顺序。
- 即:MySQL 通常先从结果集的第一行对应的表中查找数据,然后嵌套到第二行、第三行对应的表中。
- 在处理完所有表并输出选定列后,MySQL 会回溯表列表,寻找更多匹配的行,然后读取下一行数据,继续上述过程。
- (此特性对于分析多表 JOIN 的驱动表选择至关重要)
💡 辅助工具提示:MySQL Workbench 提供了可视化的解释功能,能够以图形化的方式直观呈现
EXPLAIN的输出结果,有助于快速理解复杂的执行计划。
2.输出字段字典
EXPLAIN 的每一行输出都包含关于一张表的执行细节。传统的表格格式包含以下列(Column),如果使用 FORMAT=JSON 输出,则对应特定的 JSON 属性名(JSON Name)。
以下是核心输出列的官方定义与专业解析:
| 列名 (Column) | JSON 属性名 (JSON Name) | 核心含义解析 |
|---|---|---|
id |
select_id |
SELECT 标识符。代表查询中 SELECT 的顺序号。 |
select_type |
(无) | SELECT 类型。用于区分简单查询、联合查询、子查询等复杂类型。 |
table |
table_name |
输出行对应的表名。可能包含派生表、联合结果的临时表名。 |
partitions |
partitions |
匹配的分区。记录查询命中的分区,非分区表显示为 NULL。 |
type |
access_type |
连接类型。极其重要,表示 MySQL 决定如何查找表中的行(如全表扫描 ALL、索引查找 ref 等)。 |
possible_keys |
possible_keys |
可选索引。查询可能使用到的索引列表。 |
key |
key |
实际选择的索引。优化器最终决定使用的索引。若为 NULL 则未走索引。 |
key_len |
key_length |
所选密钥的长度。表示使用的索引字节长度,可据此判断复合索引的利用情况。 |
ref |
ref |
列与索引的比较。显示哪些列或常量被用于与 key 列中的索引进行比较。 |
rows |
rows |
预计要检查的行数。优化器预估必须读取的行数,数值越小越好。 |
filtered |
filtered |
按表条件过滤的行百分比。表示经过 WHERE 条件过滤后剩余的行的百分比(最大值 100)。 |
Extra |
(无) | 附加信息。包含执行计划的额外细节(如 Using filesort, Using index 等),是性能优化的关键指标。 |
⚠️ 官方文档勘误说明:原意应为:“某些 JSON 属性可能没有对应的传统列名,且 JSON 格式输出中包含但在默认格式中不显示的属性(如成本信息),可通过指定
FORMAT=JSON查看。”
嵌套循环和行顺序即读取顺序(理解查询)
真实SQL场景示例
假设有两张表:
- 表 A(
users用户表):有 3 条数据(id: 1, 2, 3)。 - 表 B(
orders订单表):有 4 条数据(属于用户 1 的有 2 条,属于用户 2 的有 1 条,属于用户 3 的有 1 条)。 - 执行一条关联查询:
sql
SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id;
EXPLAIN 的输出结果
在 MySQL 中输入 EXPLAIN 后,它会返回如下两行(简略版):
text
+----+-------+--------+
| id | table | type | <-- (仅展示关键列)
+----+-------+--------+
| 1 | users | ALL | <-- 第一行:驱动表
| 1 | orders| ref | <-- 第二行:被驱动表
+----+-------+--------+
核心结论:EXPLAIN 输出中的第一行(users)就是外层循环,第二行(orders)就是内层循环。
⭐ 动画拆解:MySQL 底层是如何执行的?
这个过程就像写代码时的双层 for 循环。
text
【外层循环】读取第一行表 (users) 【内层循环】读取第二行表 (orders)
| |
|--- 取出 user_id = 1 ---------------->| 扫描 orders 表,找到 user_id=1 的订单 (共2条)
| | 👉 输出第一行组合 (user1 + orderA)
| | 👉 输出第二行组合 (user1 + orderB)
| | 👉 当前用户的订单找完了,内层循环结束
|
|--- 取出 user_id = 2 ---------------->| 扫描 orders 表,找到 user_id=2 的订单 (共1条)
| | 👉 输出第三行组合 (user2 + orderC)
| | 👉 当前用户的订单找完了,内层循环结束
|
|--- 取出 user_id = 3 ---------------->| 扫描 orders 表,找到 user_id=3 的订单 (共1条)
| | 👉 输出第四行组合 (user3 + orderD)
| | 👉 当前用户的订单找完了,内层循环结束
|
|--- users 表的数据也读完了,整个查询结束。
- 对应图解:在这个例子中,
users表就是驱动表(外层循环),orders表是被驱动表(内层循环)。如果users表有 100 万行,MySQL 就会把orders表扫描 100 万次(如果没有索引的话)。所以优化器总是倾向于选择数据量小、过滤后结果少的表来作为第一行(驱动表)。
3.核心输出列基准定义
在执行 EXPLAIN 后,默认输出的每一行代表一个表的访问信息。下表定义了核心输出列的含义,以及在使用 FORMAT=JSON 格式输出时所对应的属性名称:
| 列名 (Column) | JSON 属性名 (JSON Name) | 核心含义描述 |
|---|---|---|
id |
select_id |
SELECT 标识符。即查询中 SELECT 的序号。 |
select_type |
(无) | SELECT 类型。见下文详细说明。 |
table |
table_name |
输出行对应的表名。 |
partitions |
partitions |
匹配的分区。非分区表为 NULL。 |
type |
access_type |
连接类型(极其重要,如 ALL, ref, index 等)。 |
possible_keys |
possible_keys |
可选索引。查询可能使用的索引列表。 |
key |
key |
实际选择的索引。优化器最终决定使用的索引。 |
key_len |
key_length |
所选密钥的长度。用于判断复合索引的利用情况。 |
ref |
ref |
列与索引的比较。显示哪些列或常量被用于与索引比较。 |
rows |
rows |
预计要检查的行数。预估值,越小越好。 |
filtered |
filtered |
按表条件过滤的行百分比。最大值 100。 |
Extra |
(无) | 附加信息。如 Using filesort 等关键诊断信息。 |
⚠️ 官方勘误说明: 原意为:“部分属性仅存在于 JSON 格式输出中,默认表格格式不显示;若为 NULL,则代表该字段在当前 EXPLAIN 输出中无对应值。”
┌──────────────────────────────────────────────┐
│ EXPLAIN 输出结果 (相当于一张临时表) │
├──────┬────────────┬──────────┬───────────────┤
第1行 │ id=1 │ table:users│ type:ALL │ (读取表 users)│
├──────┼────────────┼──────────┼───────────────┤
第2行 │ id=1 │ table:orders│ type:ref│ (读取表 orders)│
└──────┴────────────┴──────────┴───────────────┘
│ │
│ └─ 每一行都有对应的 "table" 列,标明现在在访问哪张表
│
└─ 行与行之间的上下顺序,就是 MySQL 读表的先后顺序
核心列深度解析
==1==. id (JSON 名称: select_id)
- 概念:SELECT 查询的序列号。
- 特殊情况:如果该行涉及子查询或 UNION 合并,
id列可能会显示为NULL,或者显示为类似<unionM,N>的格式,表示这是id值为 M 和 N 的行的并集(Union)结果。
2. select_type (JSON 名称: 无)
- 概念:表示 SELECT 查询的类型,用于区分普通查询、联合查询、子查询等复杂场景。
- JSON 映射说明:在
FORMAT=JSON格式中,如果该值有对应的 JSON 属性(如dependent(true)),则会暴露该属性;如果没有,则仅在传统表格中显示。 - 类型枚举字典:
select_type 值 |
JSON 属性名 | 含义说明 |
|---|---|---|
SIMPLE |
无 | 简单 SELECT。不使用 UNION 或子查询。 |
PRIMARY |
无 | 最外层 SELECT。如果包含子查询或 UNION,最外层的查询即为 PRIMARY。 |
UNION |
无 | 联合查询。UNION 中的第二个及之后的 SELECT 语句。 |
DEPENDENT UNION |
dependent (true) |
依赖联合。UNION 中的第二个或之后的 SELECT,依赖于外部查询(相关子查询)。 |
UNION RESULT |
union_result |
联合结果。UNION 合并后的最终结果集。 |
SUBQUERY |
无 | 子查询。子查询中的第一个 SELECT。 |
DEPENDENT SUBQUERY |
dependent (true) |
依赖子查询。子查询的第一个 SELECT,依赖于外部查询。 |
DERIVED |
无 | 派生表。在 FROM 子句中生成的临时表(子查询)。 |
DEPENDENT DERIVED |
dependent (true) |
依赖派生表。依赖于另一张表的派生表。 |
MATERIALIZED |
materialized_from_subquery |
物化子查询。优化器将子查询结果物化为临时表。 |
UNCACHEABLE SUBQUERY |
cacheable (false) |
不可缓存子查询。结果无法缓存,外层查询的每一行都需要重新评估该子查询。 |
UNCACHEABLE UNION |
cacheable (false) |
不可缓存联合。属于不可缓存子查询的 UNION 中的第二个或更晚的 SELECT。 |
3. table (JSON 属性名: table_name)
核心含义:当前输出行所访问的表名称。 除了真实的物理表名外,它还可能显示为以下三种特殊临时表格式:
<unionM,N>:表示该行指的是id值为 M 和 N 的查询结果进行 UNION 并集后的结果。<derivedN>:表示该行指的是id值为 N 的派生表(Derived Table)结果。通常来源于FROM子句中的子查询。<subqueryN>:表示该行指的是id值为 N 的子查询被物化(Materialized)后的结果。
EXPLAIN 输出中 table 列的 4 种形态
graph TD
Root["EXPLAIN 输出中的 table 列"] --> Normal["1. 普通物理表"]
Root --> Special["2. 特殊临时表 (尖括号包裹)"]
Normal --> N1["形态: 真实的表名 (如 users, orders)"]
Normal --> N2["含义: 数据库中实际存在的物理表"]
Special --> U["< unionM,N >"]
Special --> D["< derivedN >"]
Special --> S["< subqueryN >"]
U --> U1["含义: id 为 M 和 N 的查询 UNION 合并后的结果"]
D --> D1["含义: 派生表。通常是 FROM 子句里的子查询结果"]
S --> S1["含义: 物化子查询。WHERE 里被优化器缓存的子查询结果"]
classDef root fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
classDef normal fill:#d9edf7,stroke:#31708f,stroke-width:2px;
classDef special fill:#dff0d8,stroke:#3c763d,stroke-width:2px;
classDef detail fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px,stroke-dasharray: 5 5;
class Root root;
class Normal,N1,N2 normal;
class Special,U,D,S special;
class U1,D1,S1 detail;
==4==. type (JSON 属性名: access_type)
核心含义:连接类型(访问类型)。这是极其关键的性能指标,代表了 MySQL 决定如何查找表中的行。
- (注:具体包含哪些类型及其性能优劣,详见后续章节的“解释连接类型”)。
==5==. possible_keys (JSON 属性名: possible_keys)
核心含义:MySQL 在优化阶段可能选择使用的索引列表(查询中涉及的列上如果有索引,就会列在这里)。
⚠️ 官方原文档纠偏与深度解析:
- 上下文独立:该列完全独立于
EXPLAIN输出中显示的表格顺序。这意味着possible_keys中列出的一些索引,在实际的表连接操作中可能无法被使用。- 值为
NULL的情况:如果没有相关索引,该列显示为NULL(或在 JSON 输出中未定义)。
- 优化建议:如果该列为
NULL,你可以检查WHERE子句,确认查询条件是否涉及了某些列。如果涉及,可以考虑为这些列创建合适的索引,然后使用EXPLAIN再次检查查询。- 查看现有索引:如果你想查看某张表上当前已经存在哪些索引,可以使用
SHOW INDEX FROM tbl_name命令。
📌 生活化类比:图书检索系统 (EXPLAIN)
你要查的内容: "苹果" (对应你的 WHERE 条件)
+-------------------------------------------------------------+
| 步骤 1: 检索器(优化器)查阅总目录 |
| 发现有两个索引可能包含"苹果"的信息: |
| |
| 👉 possible_keys = [ 拼音索引, 偏旁部首索引 ] |
| (可能用得上,也可能用不上) |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| 步骤 2: 检索器最终决定怎么翻书 |
| |
| 👉 key = 拼音索引 |
| (实际选择使用的索引,这里只选了一个) |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| 特殊情况:如果目录里根本没找到相关索引 |
| |
| 👉 possible_keys = NULL (警告:可能需要新建索引) |
| 👉 key = NULL (实锤:正在全表扫描,极慢!) |
+-------------------------------------------------------------+
graph TD
Start["执行 EXPLAIN 分析 SQL"] --> Check{"检查 WHERE / JOIN 条件列"}
Check -->|条件列上存在索引| HasIndex["possible_keys 列出候选索引"]
Check -->|条件列上无索引| NullIndex["possible_keys 值为 NULL"]
HasIndex --> Optimizer["优化器进行成本计算 (Cost)"]
Optimizer -->|选择最佳方案| Chosen["key 列显示: 实际选用的索引"]
Optimizer -->|认为全表扫描更快| Ignored["key 列显示: NULL (放弃使用索引)"]
NullIndex --> Action1["💡 优化建议: 针对条件列创建新索引"]
Action1 --> ReRun["再次执行 EXPLAIN 验证"]
Chosen --> CheckTable["💡 查看表内现有索引: SHOW INDEX FROM tbl_name"]
Ignored --> CheckTable
classDef warn fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px;
classDef process fill:#e2e3e5,stroke:#6c757d,stroke-width:2px;
classDef info fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px;
class NullIndex,Ignored warn;
class HasIndex,Chosen success;
class Start,Check,Optimizer process;
class Action1,ReRun,CheckTable info;
┌────────────────────────────────────────────────────────┐
│ 🌟 possible_keys 的三大核心认知 │
├────────────────────────────────────────────────────────┤
│ 1. 它是“候选名单”:只代表有资格参与,不代表一定会被用。│
│ │
│ 2. 它是“独立计算”的:不看其他表的连接顺序,只管自己这 │
│ 张表有哪些条件涉及了索引。 │
│ │
│ 3. 它和 key 的区别:possible_keys 是“海选”,key 是最终│
│ “冠军”。如果海选有,但 key 是 NULL,说明全表扫描更 │
│ 划算;如果海选直接是 NULL,说明你必须加索引了。 │
└────────────────────────────────────────────────────────┘
==6==. key (JSON 属性名: key)
核心含义:实际使用的索引(密钥)。表示 MySQL 优化器最终决定在执行查询时采用的索引。
① 与 possible_keys 的关系与特例
- 通常情况下,
key列的值必定包含在possible_keys(候选索引)列表中。 - 特例情况(极其重要):
key列可能会显示一个不在possible_keys列表中的索引。这通常发生在覆盖索引(Covering Index)的场景下。即:虽然没有任何索引能完美用于WHERE条件过滤(不用于确定要检索哪些行),但查询所需要读取的所有列都恰好包含在某一个索引中。此时,直接扫描该索引树(全索引扫描)比扫描真实的数据行更高效,因此优化器会直接使用该索引。
② 关于 InnoDB 引擎的特殊行为
- 对于 InnoDB 表,即使查询没有显式选择二级索引(辅助索引),而是选择了主键,
key列也可能显示主键名。这是因为 InnoDB 的二级索引的叶子节点存储的就是主键值。如果 MySQL 找不到高效的可用索引来执行查询,key列将显示为NULL(代表未使用索引,通常伴随全表扫描)。
③ 强制索引提示 (Index Hints)
- 如果你发现优化器没有选择最理想的索引,你可以使用强制索引提示来干预优化器的决策:
FORCE INDEX:强制使用指定索引。USE INDEX:建议使用指定索引。IGNORE INDEX:强制忽略指定索引。
④ 索引统计信息与优化
- MyISAM 表:对于 MyISAM 存储引擎的表,运行
ANALYZE TABLE语句有助于优化器收集更准确的数据分布统计信息,从而选择更合适的索引。(MyISAM 表也可以使用myisamchk --analyze命令达到同样效果)。
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📌 EXPLAIN 之 key 列核心逻辑拆解 │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────┐
▼ ▼
[ 正常情况 ] [ 特殊情况 ]
key 存在于 possible_keys 列表中 key 不在 possible_keys 中
│ │
├─ 🟢 key = 索引名 ├─ 🟣 覆盖索引 (Covering Index)
│ 说明:优化器在候选索引中,最终选中了它。 │ 说明:虽然 WHERE 没用到,但
│ │ SELECT 的列全在索引里。
│ │ 扫索引树比回表更高效!
│ │
└─ 🔴 key = NULL └─ ⚙️ 强制干预工具
说明:没有合适索引,或全表扫描 FORCE INDEX / IGNORE INDEX
更划算(性能最差警示)。 可以人为改变优化器选择。
graph TD
Start["查看 EXPLAIN 的 key 列"] --> CheckNull{"key 是否为 NULL?"}
CheckNull -->|"是 (YES)"| NullKey["🔴 未使用索引"]
NullKey --> ExplainNull["含义: 通常是全表扫描 (性能差)<br>或者优化器认为全表扫描更划算"]
ExplainNull --> Action1["💡 优化建议: 检查统计信息<br>或尝试使用 FORCE INDEX 强制走索引"]
CheckNull -->|"否 (NO)"| CompareKeys{"key 是否在<br>possible_keys 列表中?"}
CompareKeys -->|"在 (YES)"| NormalKey["🟢 正常使用索引"]
NormalKey --> ExplainNormal["含义: 优化器在候选名单中<br>选出了最优的索引"]
CompareKeys -->|"不在 (NO)"| CoverIndex["🟣 覆盖索引 (Covering Index)"]
CoverIndex --> ExplainCover["含义: 虽然 WHERE 没用到,<br>但 SELECT 需要的列全在索引中。<br>直接扫索引树,免去回表操作,极快!"]
style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
style NullKey fill:#f8d7da,stroke:#dc3545,stroke-width:2px
style NormalKey fill:#d4edda,stroke:#28a745,stroke-width:2px
style CoverIndex fill:#e2d9f3,stroke:#6f42c1,stroke-width:2px
style Action1 fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5
7. ref (JSON 属性名: ref)
核心含义:显示哪些列或常量被用于与 key 列中命名的索引进行比较,从而从表中读取数据行。
-
常规情况:如果显示的是具体的列名或
const(常量),说明查询利用这些值去索引中匹配数据。 -
函数特例:如果该列的值显示为
func,则表示用于比较的值是某个函数的结果。- 排查方法:想知道具体是什么函数,需要在执行
EXPLAIN后,紧接着执行SHOW WARNINGS命令,通过查看扩展的EXPLAIN输出来获取详情。该函数可能是一个算术运算符等。
┌─────────────────────────────────────────────────────────────────────────────┐ │ 📌 EXPLAIN 之 `ref` 列逻辑拆解 (谁在和索引“比大小”?) │ └─────────────────────────────────────────────────────────────────────────────┘ │ ┌────────────────────────────┴────────────────────────────┐ ▼ ▼ [ 常规情况 ] [ 特殊情况 ] 显示具体的列名或常量 显示为 'func' │ │ ├─ 🟢 ref = const ├─ 🟣 ref = func │ 含义:用一个固定的常量去索引里查。 │ 含义:用于比较的值,是某个 │ 例如:WHERE id = 1 │ 函数计算结果。 │ │ ├─ 🟢 ref = db.table.column │ │ 含义:拿前一张表(驱动表)的某列, └─ ⚙️ 如何排查具体是什么函数? │ 来当前表的索引里查(JOIN 时最典型)。 执行 EXPLAIN 后,立刻执行 │ 例如:ON A.id = B.user_id SHOW WARNINGS; │ (查看扩展信息,会显示具体 └─ 🟢 ref = NULL 算术运算符等细节) 含义:没有用到等值比较(如用了范围查询)。graph TD Start["查看 EXPLAIN 的 ref 列"] --> TypeCheck{"ref 的值是什么?"} TypeCheck -->|"具体列名"| ColumnRef["🟢 常规比较 (比如 A.id)"] ColumnRef --> Detail1["含义: 拿驱动表的数据列<br>来匹配当前表的索引"] TypeCheck -->|"const"| ConstRef["🟢 常量比较"] ConstRef --> Detail2["含义: 使用固定的常量值<br>直接去索引中精确查找"] TypeCheck -->|"func"| FuncRef["🟣 函数计算特例"] FuncRef --> Detail3["含义: 比较的值是函数运算结果"] Detail3 --> Action["⚙️ 排查步骤:<br>执行 SHOW WARNINGS 查看具体函数"] TypeCheck -->|"NULL"| NullRef["⚪ 无等值比较"] NullRef --> Detail4["含义: 可能是范围查询 (如 >, <)<br>或者没有用到索引比较"] style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px style ColumnRef fill:#d4edda,stroke:#28a745,stroke-width:2px style ConstRef fill:#d4edda,stroke:#28a745,stroke-width:2px style FuncRef fill:#e2d9f3,stroke:#6f42c1,stroke-width:2px style NullRef fill:#e2e3e5,stroke:#6c757d,stroke-width:2px style Action fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5 - 排查方法:想知道具体是什么函数,需要在执行
==8==. rows (JSON 属性名: rows)
核心含义:MySQL 预估为了执行此查询,必须检查的行数(即扫描行数)。
- 注意(针对 InnoDB 引擎):对于 InnoDB 表,
rows列的数字是一个预估值,可能并不总是完全准确。它是基于统计信息计算出来的,数值越小越好。
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📌 EXPLAIN 之 `rows` 列核心逻辑拆解 (预估扫描行数) │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────┐
▼ ▼
[ 本质含义 ] [ 引擎差异与准确性 ]
│ │
├─ 🎯 它是什么? ├─ 🛑 InnoDB 引擎 (最常用)
│ 优化器认为执行该查询,必须检查 │ 数值是【预估值】(Estimated)
│ 的行数 (即扫描了多少行数据)。 │ 基于采样统计得出,可能不准。
│ │
├─ 📉 评估标准 └─ ⚙️ 为什么不准?
│ 【数值越小越好】。 因为 InnoDB 面对高频增删改时,
│ 数值越大,说明扫描的数据量越大, 不会实时更新精确的行数,而是
│ 磁盘 I/O 越高,性能越差。 采用抽样估算来保证性能。
│
└─ ⚠️ 误区提示
它不代表最终返回给用户的行数。
它代表的是“为了找到结果,底层翻了多少行”。
graph TD
Start["查看 EXPLAIN 的 rows 列"] --> Core["🎯 核心含义: 预估需要扫描的行数"]
Core --> Rule["📉 性能判断原则"]
Rule --> Better["🟢 数值越小越好<br>(扫描行数少,I/O 压力小)"]
Rule --> Worse["🔴 数值极大 (如百万级)<br>(扫描行数多,性能极差)"]
Core --> Engine{"针对什么存储引擎?"}
Engine -->|"InnoDB (默认)"| InnoDB["🛑 预估值 (Estimated)"]
InnoDB --> Explain1["基于索引统计信息估算<br>并非 100% 精确"]
InnoDB --> Action["💡 建议: 定期执行 ANALYZE TABLE<br>更新统计信息,帮助优化器算得更准"]
Engine -->|"MyISAM"| MyISAM["✅ 精确值 (Exact)"]
MyISAM --> Explain2["通常直接读取表的真实行数<br>相对准确"]
style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
style Core fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style Better fill:#d4edda,stroke:#28a745,stroke-width:2px
style Worse fill:#f8d7da,stroke:#dc3545,stroke-width:2px
style InnoDB fill:#fff3cd,stroke:#ffc107,stroke-width:2px
style Action fill:#e2e3e5,stroke:#6c757d,stroke-width:1px,stroke-dasharray: 5 5
==9==. Extra (JSON 属性名: 无)
核心含义:附加信息。本列包含 MySQL 如何解析查询的额外细节,是诊断 SQL 性能的黄金字段(例如 Using filesort, Using index 等)。
- JSON 映射说明:该列没有单一对应的 JSON 属性。然而,这里出现的某些值(如
Using filesort)在 JSON 格式输出中,会作为独立的属性名暴露出来(值为true),或者直接作为属性文本(message)展示。
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📌 EXPLAIN 之 `Extra` 列核心信息分类 (附加信息诊断) │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────┐
▼ ▼
[ 🚨 性能“红灯”区 (危险) ] [ 🟢 性能“绿灯”区 (优秀) ]
│ │
├─ Using filesort ├─ Using index
│ 说明:无法利用索引完成排序,必须在内存或磁盘中 │ 说明:使用了【覆盖索引】。查询的列全在索引里,
│ 进行额外的文件排序。性能杀手! │ 不需要回表查数据,速度极快。
│ │
├─ Using temporary ├─ Using index condition
│ 说明:必须创建临时表来存储中间结果。常见于 │ 说明:使用了【索引条件下推 (ICP)】。在存储引擎
│ GROUP BY 或 DISTINCT 且无合适索引时。性能杀手! │ 层就提前过滤了数据,减少了回表次数。
│ │
└─ Using join buffer (Block Nested Loop) └─ Select tables optimized away
说明:表连接时没有用到索引,使用了 Join Buffer。 说明:优化器直接通过索引就拿到了结果
通常意味着被驱动表缺乏合适的关联索引。 (如 MIN/MAX),甚至不需要读取表数据。
graph TD
Start["查看 EXPLAIN 的 Extra 列"] --> TrafficLight{"信息诊断分类"}
TrafficLight -->|"🚨 危险信号"| RedZone["需要立即优化"]
TrafficLight -->|"🟢 良好信号"| GreenZone["表现优秀,无需干预"]
TrafficLight -->|"⚪ 其他情况"| GrayZone["特定场景 / 正常提示"]
%% 红灯区
RedZone --> Filesort["Using filesort"]
Filesort --> FS_Detail["含义: 额外排序<br>原因: ORDER BY 字段没走索引"]
Filesort --> FS_Fix["💡 优化: 为 ORDER BY 字段建索引"]
RedZone --> Temporary["Using temporary"]
Temporary --> Tmp_Detail["含义: 使用了临时表<br>原因: GROUP BY 等操作没走索引"]
Temporary --> Tmp_Fix["💡 优化: 优化 SQL 或建立联合索引"]
%% 绿灯区
GreenZone --> UsingIndex["Using index"]
UsingIndex --> UI_Detail["含义: 覆盖索引<br>表现: 无需回表,极快!"]
GreenZone --> ICP["Using index condition"]
ICP --> ICP_Detail["含义: 索引条件下推<br>表现: 存储引擎层提前过滤,减少回表"]
%% 灰灯区
GrayZone --> UsingWhere["Using where"]
UsingWhere --> UW_Detail["含义: 使用了 WHERE 过滤<br>说明: 需结合 type 列判断好坏"]
style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
style TrafficLight fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style RedZone fill:#f8d7da,stroke:#dc3545,stroke-width:2px
style Filesort fill:#f8d7da,stroke:#dc3545,stroke-width:1px
style Temporary fill:#f8d7da,stroke:#dc3545,stroke-width:1px
style GreenZone fill:#d4edda,stroke:#28a745,stroke-width:2px
style UsingIndex fill:#d4edda,stroke:#28a745,stroke-width:1px
style ICP fill:#d4edda,stroke:#28a745,stroke-width:1px
style GrayZone fill:#e2e3e5,stroke:#6c757d,stroke-width:2px
style UsingWhere fill:#e2e3e5,stroke:#6c757d,stroke-width:1px
style FS_Fix fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5
style Tmp_Fix fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5
==explain连接类型(type/access_type)==
1、 核心概念与评判标准
核心含义:type 列(JSON 属性名为 access_type)描述了 MySQL 决定如何将表连接起来(即如何查找表中的行)。
性能排序:官方列出的连接类型,自上而下性能依次递减(越往下越差)。我们在优化 SQL 时,核心目标就是让 type 尽量靠前。
2、 高优连接类型深度解析
1. system (系统表)
- 定义:表中只有一行数据(等于系统表)。
- 性质:这是
const连接类型极其特殊的一种情况,速度极快。
==2==. const (常量级查找)
-
定义:表最多只有一行匹配的行,且在查询一开始就被读取。
-
原理:因为只有一行,该行中列的值在查询的剩余部分可以被当作常量(Constant)来对待,由优化器直接优化掉。
-
性能:极快,因为只读一次。常用于通过主键(PRIMARY KEY)或唯一索引(UNIQUE)与常量值进行比较。
-
触发条件:将主键/唯一索引的所有部分与常量进行比较。
-
官方示例:
sql
-- 示例 1:单列主键直接命中 SELECT * FROM tbl_name WHERE primary_key = 1; -- 示例 2:复合主键的所有列均用常量等值匹配 SELECT * FROM tbl_name WHERE primary_key_part1 = 1 AND primary_key_part2 = 2;
3. eq_ref (等值引用 / 唯一索引关联)
-
定义:对于前几张表的每一行组合,从当前表中最多只读取一行。
-
性能:除了
system和const之外,这是最佳(最优)的连接类型。 -
触发条件:当所有被连接的列都是主键(PRIMARY KEY)或非空的唯一索引(UNIQUE NOT NULL),并且使用
=操作符进行比较时。 -
比较值来源:比较值可以是一个常量,也可以是外部表的列表达式。MySQL 会使用该表达式去
ref_table中查找。 -
官方示例:
sql
-- 示例 1:关联表使用主键/唯一索引 SELECT * FROM ref_table, other_table WHERE ref_table.key_column = other_table.column; -- 示例 2:复合唯一索引的所有部分参与等值关联 SELECT * FROM ref_table, other_table WHERE ref_table.key_column_part1 = other_table.column AND ref_table.key_column_part2 = 1;
4. ref (非唯一索引等值查找)
-
核心定义:从当前表中读取所有匹配某个索引值的行。这是基于普通(非唯一)索引进行的查找。
-
触发条件:如果使用的索引仅包含最左前缀,或者该索引不是
PRIMARY KEY或UNIQUE索引(即索引值允许重复)。 -
性能评估:虽然无法像
eq_ref那样只锁定一行,但如果该索引只匹配了少部分行,这种连接方式依然是非常不错的。 -
特例:
ref也可以用于使用=或<=>(NULL 安全等于)操作符的索引列比较。 -
官方示例:
sql
SELECT * FROM ref_table WHERE key_column = expr; SELECT * FROM ref_table, other_table WHERE ref_table.key_column = other_table.column; -- 复合索引的最左前缀匹配,且部分列为常量 SELECT * FROM ref_table, other_table WHERE ref_table.key_column_part1 = other_table.column AND ref_table.key_column_part2 = 1;
5. ref_or_null (等值查找 + NULL 查找)
-
核心定义:连接类型类似于
ref,但额外增加了对NULL值的搜索。 -
原文档纠偏:实际含义是:MySQL 会专门对
IS NULL的情况进行额外检索。 -
适用场景:这种优化最常见于解析子查询。如果你的查询条件既包含等值匹配,又允许该列为
NULL。 -
官方示例:
sql
-- 既要找等于 expr 的,也要找为空的行 SELECT * FROM ref_table WHERE key_column = expr OR key_column IS NULL;
==6==. range (范围扫描)
-
核心定义:仅检索在给定范围内的行,并使用一个索引来选择行。
-
关键特征:
key列显示实际使用的索引。key_len列包含使用的最长密钥部分的长度。ref列针对此类型通常为NULL。
-
触发条件:使用任何一个
=,<>,>,>=,<,<=,IS NULL,<=>,BETWEEN,LIKE或IN()运算符将索引列与常量进行比较。 -
官方示例:
sql
SELECT * FROM tbl_name WHERE key_column = 10; SELECT * FROM tbl_name WHERE key_column BETWEEN 10 and 20; SELECT * FROM tbl_name WHERE key_column IN (10,20,30); -- 复合索引的范围查询 SELECT * FROM tbl_name WHERE key_part1 = 10 AND key_part2 IN (10,20,30);
==7==. index (全索引扫描)
- 核心定义:连接类型与
ALL相同,但扫描的是索引树而不是数据行。 - 具体分两种情况:
- 覆盖索引扫描(好情况):如果该索引是覆盖索引,即查询所需要的所有列都在索引中,那么只扫描索引树即可。此时
Extra列会显示Using index。因为索引通常比表数据小得多,所以这种扫描通常比ALL快。 - 按索引顺序全表扫描(较差情况):利用索引按顺序查找数据行。此时
Extra列中不会出现Using index。
- 覆盖索引扫描(好情况):如果该索引是覆盖索引,即查询所需要的所有列都在索引中,那么只扫描索引树即可。此时
- 触发条件:当查询仅选择属于单一索引的列时,MySQL 可能会选择这种方式。
==8==. ALL (全表扫描)
- 核心定义:对之前表中的每个行组合进行全表扫描来查找匹配的行。
- 性能评价:通常非常糟糕(Very Bad)。如果
table是第一个没有标记为const的表,在大多数其他情况下,这意味着性能灾难。 - 优化建议:通常,你应通过添加合适的索引来避免全表扫描,使得基于常量值或早期表中的列值来检索行成为可能。
性能图
text
┌───────────────────────────────────────────────────────────────────────────────────┐
│ 📌 MySQL EXPLAIN `type` (连接类型) 性能全景精简版 │
│ (优化目标:守住绿黄区,消灭红区!) │
└───────────────────────────────────────────────────────────────────────────────────┘
🟢 【顶级性能区】 (极快,基本不用管)
───────────────────────────────────────────────────────────────────────────────────
1. system : 表只有一行(系统表)。极速。
2. const : 主键/唯一索引 等值查常量。只读一次,极速。
3. eq_ref : 多表 JOIN 时,使用主键/唯一非空索引关联。极优。
───────────────────────────────────────────────────────────────────────────────────
🟡 【良好性能区】 (常规优秀查询,日常追求的目标)
───────────────────────────────────────────────────────────────────────────────────
4. ref : 普通索引等值查找。可能匹配多行,速度尚可。
5. ref_or_null : 类似 ref,但额外包含 IS NULL 的查找。
───────────────────────────────────────────────────────────────────────────────────
🟠 【警告区】 (扫描范围开始变大,需警惕)
───────────────────────────────────────────────────────────────────────────────────
6. range : 索引范围扫描 (BETWEEN, >, <, IN 等)。
───────────────────────────────────────────────────────────────────────────────────
🔴 【危险区】 (性能断崖式下跌,必须优化)
───────────────────────────────────────────────────────────────────────────────────
7. index : 全索引扫描。
├─ 🟢 覆盖索引 (Extra: Using index) -> 极快,不用回表。
└─ 🔴 非覆盖索引 (Extra: 无) -> 较慢,要按索引顺序回表。
8. ALL : 全表扫描。💀 性能最差,磁盘 I/O 极高,全力避免!
───────────────────────────────────────────────────────────────────────────────────
graph TD
subgraph 顶级性能区
A1["1. system (单行系统表)"]
A2["2. const (常量/唯一索引等值)"]
A3["3. eq_ref (主键/唯一非空 JOIN)"]
end
subgraph 良好性能区
B1["4. ref (普通索引等值)"]
B2["5. ref_or_null (等值 + NULL)"]
end
subgraph 警告区
C1["6. range (索引范围扫描)"]
end
subgraph 危险区
D1["7. index (全索引扫描 - 覆盖快/非覆盖慢)"]
D2["8. ALL (全表扫描 - 最差!)"]
end
%% 层级流向
A1 --> A2 --> A3
A3 --> B1
B1 --> B2
B2 --> C1
C1 --> D1 --> D2
%% 样式定义
classDef top fill:#d4edda,stroke:#28a745,stroke-width:2px;
classDef good fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
classDef warn fill:#ffcc99,stroke:#fd7e14,stroke-width:2px;
classDef danger fill:#f5c6cb,stroke:#dc3545,stroke-width:2px;
class A1,A2,A3 top;
class B1,B2 good;
class C1 warn;
class D1,D2 danger;
==Type列性能金字塔==(要背)
┌─────────────────────────────────────────────────────────────┐
│ EXPLAIN `type` 列 性能金字塔 (越往上越牛) │
└─────────────────────────────────────────────────────────────┘
▲
╱ ╲
╱ ╲
╱ 1 ╲ <--- system (表里只有一行)
╱─────╲
╱ ╲
╱ 2 ╲ <--- const (主键/唯一索引 等值查常量)
╱───────────╲
╱ ╲
╱ 3 ╲ <--- eq_ref (多表 JOIN 时,使用主键/唯一非空索引)
╱─────────────────╲
╱ ╲
╱ 4 ╲ <--- ref (非唯一索引等值查找)
╱───────────────────────╲
╱ ╲
╱ 5 ╲ <--- range (索引范围扫描)
╱─────────────────────────────╲
╱ ╲
╱ 6 ╲ <--- index (全索引扫描)
╱───────────────────────────────────╲
╱ ╲
╱ 7 ╲ <--- ALL (全表扫描,性能最差)
╱─────────────────────────────────────────╲
graph TD
A["👑 system<br>(表只有一行,特殊情况)"] --> B["🚀 const<br>(主键/唯一索引 等值查常量,只读一次)"]
B --> C["⚡ eq_ref<br>(多表 JOIN,主键/唯一非空索引等值关联)"]
C --> D["✅ ref<br>(非唯一索引等值查找)"]
D --> E["🟡 range<br>(索引范围扫描,如 >, <, BETWEEN)"]
E --> F["🟠 index<br>(全索引扫描,扫整个索引树)"]
F --> G["💀 ALL<br>(全表扫描,性能最差,必须优化)"]
style A fill:#28a745,color:#fff,stroke:#1e7e34,stroke-width:2px
style B fill:#20c997,color:#fff,stroke:#17a2b8,stroke-width:2px
style C fill:#17a2b8,color:#fff,stroke:#138496,stroke-width:2px
style D fill:#ffc107,color:#333,stroke:#d39e00,stroke-width:2px
style E fill:#fd7e14,color:#fff,stroke:#e36a10,stroke-width:2px
style F fill:#dc3545,color:#fff,stroke:#bd2130,stroke-width:2px
style G fill:#7b1fa2,color:#fff,stroke:#4a148c,stroke-width:2px
多表join实战优化案例
1. 核心概念:估算连接质量(成本计算)
在多表嵌套循环连接(Nested Loop Join)中,单看某一行的 rows 无法准确评估总成本。优化器评估成本的逻辑大致为:
- 总扫描行数估算:将
EXPLAIN输出中各个表的rows值相乘。这个乘积能大致告诉你 MySQL 为了执行该查询,底层需要检查的总行数。 - 系统变量限制:系统变量
max_join_size利用这个行积来判断查询是否过大,从而中止那些预估需要检查数十亿行的荒谬查询。
2. 优化前置条件与初始状态
假设我们有如下 4 表关联查询语句:
sql
EXPLAIN SELECT tt.TicketNumber, tt.TimeIn, ... , et.COUNTRY, et_1.COUNTRY, do.CUSTNMBR
FROM tt, et, et AS et_1, do
WHERE tt.SubmitTime IS NULL
AND tt.ActualPC = et.EMPLOYID
AND tt.AssignedPC = et_1.EMPLOYID
AND tt.ClientID = do.CUSTNMBR;
表结构与索引现状:
- 关联字段数据类型:
tt表关联字段为CHAR(10),而et和do表主键为CHAR(15)。 - 初始
EXPLAIN表现:由于WHERE条件导致驱动表全表扫描,且字符长度不匹配导致被驱动表索引失效,所有表的type均为ALL。 - 性能灾难:
rows乘积为74 × 2135 × 74 × 3872 = 45,268,558,720行(450亿次检查),产生严重的笛卡尔积。MySQL 需要极长的时间才能执行完毕。
3. 逐步优化过程与 EXPLAIN 演变
🚀 第一次优化:消除字段长度不匹配(单字段)
-
问题诊断:
tt.ActualPC(CHAR(10)) 与et.EMPLOYID(CHAR(15)) 长度不一致,导致 MySQL 无法利用索引。当类型相同但尺寸不同,或存在隐式类型转换时,均会导致索引失效。 -
优化操作:
sql
ALTER TABLE tt MODIFY ActualPC VARCHAR(15); -
EXPLAIN 变化:
et表的type从ALL变为eq_ref(通过主键关联),rows降为 1。- 扫描行积骤减至约 6 亿行,性能提升了 74 倍,查询时间从数小时/直接崩溃降至几秒钟。
🚀 第二次优化:消除所有关联字段长度不匹配
-
优化操作:将
tt表另外两个关联字段AssignedPC和ClientID的长度也修改为 15。sql
ALTER TABLE tt MODIFY AssignedPC VARCHAR(15), MODIFY ClientID VARCHAR(15); -
EXPLAIN 变化:
et_1和do表的type均变为eq_ref,rows均为 1。tt表的type变为ref(普通索引查找),rows降至 52。此时连接结构已非常优秀。
🚀 第三次优化:更新统计信息(终极补丁)
- 问题诊断:默认情况下,MySQL 假设索引列中的数据是均匀分布的。如果分布极度倾斜,优化器会做出错误的表连接顺序判断。
- 优化操作:执行
ANALYZE TABLE tt;,告知 MySQL 重新分析键的分布情况。 - 最终 EXPLAIN 表现:
- 驱动表
tt为ALL(因为SubmitTime IS NULL过滤性差,全表扫可能更优)。 - 所有被驱动表(
et,et_1,do)均为eq_ref,rows全部为 1。此时连接达到完美状态。
- 驱动表
==面试题==:
第一关:B+ 树长什么样?(为什么磁盘 I/O 少?)
核心思想:把树变“胖”,把树变“矮”。
text
【 B+ 树结构示意图 (非叶子节点只存索引) 】
[ 15 | 30 ] <--- 第1层:根节点 (常驻内存)
/ |
/ |
[ 5 | 10 ] [ 20 | 25 ] [ 35 | 40 ] <--- 第2层:树干节点 (磁盘块)
/ | / | / |
... ... ... ... ... ... ... ... ...
/ | / | / |
[1,2] [5,6] [9,10] [15,18][20,22][25,28][30,33][35,38][40,45] <--- 第3层:叶子节点(存数据)
| | | | | | | | |
+------+------+------+------+------+------+------+------+
(双向链表,支持范围查询)
graph TD
%% 根节点 (第一层)
Root["[ 15 | 30 ]<br>(根节点: 常驻内存)"]
%% 树干节点 (第二层)
L2_1["[ 5 | 10 ]<br>(树干: 存索引)"]
L2_2["[ 20 | 25 ]<br>(树干: 存索引)"]
L2_3["[ 35 | 40 ]<br>(树干: 存索引)"]
Root --> L2_1
Root --> L2_2
Root --> L2_3
%% 叶子节点 (第三层)
L3_1["[1,2]"]
L3_2["[5,6]"]
L3_3["[9,10]"]
L3_4["[15,18]"]
L3_5["[20,22]"]
L3_6["[25,28]"]
L3_7["[30,33]"]
L3_8["[35,38]"]
L3_9["[40,45]"]
L2_1 --> L3_1
L2_1 --> L3_2
L2_1 --> L3_3
L2_2 --> L3_4
L2_2 --> L3_5
L2_2 --> L3_6
L2_3 --> L3_7
L2_3 --> L3_8
L2_3 --> L3_9
%% 叶子节点之间的链表关系 (用虚线表示)
L3_1 -.-> L3_2 -.-> L3_3 -.-> L3_4 -.-> L3_5 -.-> L3_6 -.-> L3_7 -.-> L3_8 -.-> L3_9
%% 节点样式
classDef root fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
classDef branch fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef leaf fill:#d4edda,stroke:#28a745,stroke-width:2px;
class Root root;
class L2_1,L2_2,L2_3 branch;
class L3_1,L3_2,L3_3,L3_4,L3_5,L3_6,L3_7,L3_8,L3_9 leaf;
💡 为什么快?
- 极度矮胖:假设一个节点 16KB,一个主键+指针占 10 字节,一个节点能存 1600 个指针。3 层高的 B+ 树,能存
1600 * 1600 * 1600约等于 40 亿条数据! - 磁盘 I/O 极少:找一条数据,只需要从根节点到叶子节点,只发生 3 次磁盘交互。相比二叉树动辄二三十次的 I/O,简直是降维打击。
第二关:聚簇索引 vs 二级索引(回表与覆盖索引)
核心思想:主键索引是“字典正文”(存全量数据),普通索引是“目录”(只存主键)。
text
【 聚簇索引 (主键 id) 】 【 二级索引 (普通字段 name) 】
[ 根节点: id=30 ] [ 根节点: name='Bob' ]
| |
----------------- -------------------------
| | | |
[ id=10 ] [ id=50 ] [ Alice ] [ Charlie ]
| | | |
----------- ----------- ----------- -----------
| | | | | | | |
[id=1, [id=10, [id=50, [id=60, [Alice, [Bob, [Charlie, [David,
data..] data..] data..] data..] id=2] id=30] id=50] id=60]
(叶子节点包含完整的一行数据) (叶子节点只包含 name + 主键id)
graph TD
subgraph 聚簇索引_主键id
A_Root["根节点: id=30"]
A_B1["分支: id=10"]
A_B2["分支: id=50"]
A_Root --> A_B1
A_Root --> A_B2
A_L1["[id=1, 完整data...]"]
A_L2["[id=10, 完整data...]"]
A_L3["[id=50, 完整data...]"]
A_L4["[id=60, 完整data...]"]
A_B1 --> A_L1
A_B1 --> A_L2
A_B2 --> A_L3
A_B2 --> A_L4
A_L1 -.-> A_L2 -.-> A_L3 -.-> A_L4
end
subgraph 二级索引_普通字段name
B_Root["根节点: name='Bob'"]
B_B1["分支: Alice"]
B_B2["分支: Charlie"]
B_Root --> B_B1
B_Root --> B_B2
B_L1["[Alice, id=2]"]
B_L2["[Bob, id=30]"]
B_L3["[Charlie, id=50]"]
B_L4["[David, id=60]"]
B_B1 --> B_L1
B_B1 --> B_L2
B_B2 --> B_L3
B_B2 --> B_L4
B_L1 -.-> B_L2 -.-> B_L3 -.-> B_L4
end
%% 样式定义
classDef rootStyle fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
classDef branchStyle fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef leafStyleA fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
classDef leafStyleB fill:#d4edda,stroke:#28a745,stroke-width:2px;
class A_Root,B_Root rootStyle;
class A_B1,A_B2,B_B1,B_B2 branchStyle;
class A_L1,A_L2,A_L3,A_L4 leafStyleA;
class B_L1,B_L2,B_L3,B_L4 leafStyleB;
💡 什么是回表?
- 你在【二级索引】里查到了
name='Bob',拿到了它的主键id=30。 - 但是你需要
SELECT age字段,二级索引里没有。你只能拿着id=30再去【聚簇索引】里查一遍,拿到完整数据。这就是“回表”。
💡 什么是覆盖索引?
- 如果你写
SELECT id, name FROM table WHERE name = 'Bob'。 - 你要的
id和name在【二级索引】的叶子节点里全都有。MySQL 直接返回,不需要回表。这就是覆盖索引(对应EXPLAIN里的Extra: Using index)。
第三关:最左前缀原则(联合索引的底层排序)
核心思想:先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。
假设你建了联合索引 INDEX(a, b, c),数据在底层的排列顺序是这样的:
text
【 联合索引 (a, b, c) 叶子节点底层排序图 】
| a | b | c | 主键 id (回表用) |
|-----|-----|-----|-----------------|
| 1 | 10 | 50 | 100 |
| 1 | 10 | 99 | 200 | <-- a相同(都是1),按b排序(b=10)
| 1 | 20 | 30 | 300 | <-- b相同,按c排序
| 2 | 5 | 80 | 400 | <-- a变了,b重新开始排
| 2 | 10 | 20 | 500 |
| 3 | 50 | 70 | 600 |

💡 为什么查 b 用不上索引?
- 你问:
WHERE b = 10。 - 在这个“大字典”里,a 是有顺序的(1,1,1,2,2,3…),但 b 的全局顺序是乱的(10, 10, 20, 5, 10, 50)。因为 a=1 的 b=20 比 a=2 的 b=5 排在前面。MySQL 没法利用 B+ 树查找,只能全表扫。
💡 为什么查 a, b 就能用上?
- 你问:
WHERE a = 2 AND b = 10。 - MySQL 先在 B+ 树里定位到
a=2的区域,在这个区域里,b是有序的(5, 10)。可以直接利用二分查找快速定位。
💡 为什么遇到范围查询 a > 1 AND b = 10 就断了?
a > 1找出了 a=2 和 a=3 的所有数据。- 在 a=2 到 a=3 这个跨度里,
b的值有 5, 10, 50… 又变成全局无序的了。所以b的索引失效,发生部分索引失效。
📝 面试终极拷问:
面试官:“如果我对 (a, b, c) 建立索引,执行 SELECT * FROM t WHERE a=1 AND c=3,能用到索引吗?”
- 你的回答:“能用上,但只能用
a的部分。因为跳过了b,c就无法利用索引的有序性了。但因为a=1已经过滤了大部分数据,所以还是比全表扫快。不过在EXPLAIN里,key_len的长度只会算上a的部分。”