MySQL 执行计划(explain)

作者:

在

MySQL 8.0 官方文档解析:理解查询执行计划(10.8)

1、 核心概念:什么是查询执行计划(EXPLAIN Plan)

在 MySQL 中,查询执行计划(Query Execution Plan)是指优化器(Optimizer)为了高效执行一条 SQL 查询,在综合评估了表结构、列定义、索引细节以及 WHERE 子句条件后,所选择的一组最优操作集合。

  • 工作原理:优化器会在多种可用的查找技术中进行成本计算与抉择。例如,对于超大表的查询,优化器会尝试避免全表扫描;对于包含多个连接(JOIN)的查询,优化器会评估不同的表连接顺序,而不是穷举所有组合。
  • EXPLAIN Plan:优化器最终选择的这套最高效的操作集合,即为执行计划,官方也称之为 EXPLAIN 计划。

2、 核心目标:为什么要使用 EXPLAIN

EXPLAIN 语句的核心作用在于透视 SQL 的底层执行逻辑。开发者查阅执行计划的主要目标包括:

  1. 识别性能瓶颈:准确找出查询计划中指示需要优化的环节(如索引失效、全表扫描、文件排序等)。
  2. 指导优化动作:结合对 SQL 语法和索引原理的掌握,识别出低效的执行方案,并据此采取行动(如重构 SQL、调整或新增索引)来改进执行计划。

3、 知识图谱:本节结构概览

官方文档在“理解查询执行计划”大类下,按进阶逻辑划分为五个子模块:

  1. 10.8.1 用 EXPLAIN 优化查询:基础用法及适用场景。
  2. 10.8.2 EXPLAIN 输出格式:传统表格格式的字段解析(核心重点)。
  3. 10.8.3 扩展 EXPLAIN 输出格式:如 JSON 格式输出,提供更详尽的成本信息。
  4. 10.8.4 获取命名连接的执行计划信息:针对特定连接(Connection ID)的追踪。
  5. 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、 核心应用场景

  1. 索引优化指导:通过 EXPLAIN 的输出,可精准定位查询的瓶颈,从而判断应该在何处添加索引,使语句能够通过索引查找数据,显著提升执行速度。
  2. 表连接顺序优化:可用于检查优化器(Optimizer)是否以最优顺序连接了多张表。虽然可以通过 STRAIGHT_JOIN 提示词强制优化器按 SELECT 语句中表出现的顺序进行连接,但需注意,这可能会禁用半连接转换,从而阻止索引的使用(详见 15.2.13 和 10.2.2.1 节)。
  3. 诊断索引失效:若出现预期内的索引未被使用的情况,应运行 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 在优化阶段可能选择使用的索引列表(查询中涉及的列上如果有索引,就会列在这里)。

⚠️ 官方原文档纠偏与深度解析:

  1. 上下文独立:该列完全独立于 EXPLAIN 输出中显示的表格顺序。这意味着 possible_keys 中列出的一些索引,在实际的表连接操作中可能无法被使用。
  2. 值为 NULL 的情况:如果没有相关索引,该列显示为 NULL(或在 JSON 输出中未定义)。
    • 优化建议:如果该列为 NULL,你可以检查 WHERE 子句,确认查询条件是否涉及了某些列。如果涉及,可以考虑为这些列创建合适的索引,然后使用 EXPLAIN 再次检查查询。
  3. 查看现有索引:如果你想查看某张表上当前已经存在哪些索引,可以使用 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 相同,但扫描的是索引树而不是数据行。
  • 具体分两种情况:
    1. 覆盖索引扫描(好情况):如果该索引是覆盖索引,即查询所需要的所有列都在索引中,那么只扫描索引树即可。此时 Extra 列会显示 Using index。因为索引通常比表数据小得多,所以这种扫描通常比 ALL 快。
    2. 按索引顺序全表扫描(较差情况):利用索引按顺序查找数据行。此时 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        |

image-20261019003024604

💡 为什么查 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 的部分。”