--- tags: [mysql, explain, execution-plan, index-pushdown, filesort] create time: 2026-08-08 18:00 update time: 2026-08-08 18:00 --- # Explain 执行计划解读 ## 概述 `EXPLAIN` 是 MySQL 提供的最重要的性能诊断工具。它在 SQL 真正执行之前,模拟优化器生成 SQL 的执行计划,揭示查询是如何使用索引、如何连接表、是否有临时表和文件排序等关键信息。掌握 EXPLAIN 输出各字段的含义,是将经验式调优转化为精准打击的前提。 ## 核心原理 ### 执行计划各字段详解 #### type 字段 —— 访问类型(性能由好到差) type 描述了 MySQL 如何访问表中的数据,是评估查询质量的首要指标。 | type | 名称 | 含义 | 性能 | |------|------|------|------| | `system` | 系统表 | 表仅有一行(system 表),常数级读取 | ★★★★★ | | `const` | 常量 | 最多匹配一行,通过主键/唯一索引一次性定位 | ★★★★★ | | `eq_ref` | 等值引用 | 前一个表的每一行,当前表通过唯一索引匹配一行 | ★★★★☆ | | `ref` | 引用 | 通过非唯一索引或主键的前缀匹配多行 | ★★★☆☆ | | `range` | 范围 | 索引范围扫描,如 BETWEEN、IN、>、< | ★★☆☆☆ | | `index` | 全索引扫描 | 遍历整个索引树,不走表数据 | ★★☆☆☆ | | `ALL` | 全表扫描 | 逐行扫描表数据,未使用任何索引 | ☆☆☆☆☆ | ```mermaid graph LR A["system"] -->|"最优"| B["const"] B --> C["eq_ref"] C --> D["ref"] D --> E["range"] E --> F["index"] F --> G["ALL"] G -->|"最差"| H["需要优化"] style A fill:#90ee90 style B fill:#90ee90 style C fill:#90ee90 style D fill:#ffeb99 style E fill:#ffeb99 style F fill:#ffcc99 style G fill:#ff9999 style H fill:#ff6666 ``` > [!TIP] 面试常考点 > 为什么 index 比 ALL 好?`index` 虽然是全表扫描级别,但它只扫描索引树(通常为有序排列,可利用顺序 IO),而 `ALL` 需要扫描数据行(随机 IO)。所以 `index < ALL`。 #### select_type 字段 —— 查询类型 | select_type | 含义 | |-------------|------| | `SIMPLE` | 简单查询,不包含子查询或 UNION | | `PRIMARY` | 最外层查询(配合 SUBQUERY/UNION 等出现) | | `SUBQUERY` | 子查询中的 SELECT(非 FROM 子句中) | | `DERIVED` | 派生表(FROM 子句中的子查询) | | `UNION` | UNION 中的第二个及以后的 SELECT | | `UNION RESULT` | UNION 的结果集 | ```sql -- 示例:复杂查询的 select_type SELECT * FROM orders o WHERE o.user_id IN ( SELECT u.id FROM users u WHERE u.status = 1 ) UNION SELECT * FROM orders_archive WHERE archived = 1; -- 输出行的 select_type 依次为:PRIMARY / SUBQUERY / UNION / UNION_RESULT ``` #### possible_keys vs key vs key_len - **possible_keys**:优化器认为可能用到的索引列表(只是"候选",不代表最终会选用) - **key**:实际选择的索引 - **key_len**:使用的索引长度(字节数),反映索引列参与的程度 ```sql -- 复合索引 (name VARCHAR(64), age INT) -- utf8mb4 每个字符 4 字节,name 的 key_len = 64 * 4 = 256 字节 -- 加上变长标记 1 字节 + NULL 标记 1 字节 = 258 -- 如果 key_len = 258,说明用了 name 列 -- 如果 key_len = 262 (= 258 + 4),说明同时用了 name 和 age SELECT * FROM users WHERE name = 'Alice' AND age = 25; -- key: idx_name_age, key_len: 262 → 两个列都用上了 ``` #### rows × filtered —— 行数估算 - **rows**:优化器估算的需要扫描的行数(越小越好) - **filtered**:按表条件筛选后,留存行的百分比(0~100) - **rows × filtered%** ≈ 实际需要处理的行数 > [!WARNING] 估算偏差 > rows 和 filtered 是统计采样估算值,并非精确计数。如果表的统计信息过期,可能导致严重偏差。执行 `ANALYZE TABLE t;` 可以更新统计信息。 ### Extra 常见值解析 | Extra 值 | 含义 | 建议 | |----------|------|------| | `Using where` | 存储引擎返回数据后,Server 层再做 WHERE 过滤 | 正常,但如果没走索引说明需要加索引 | | `Using index` | 覆盖索引,无需回表 | 最好的情况 | | `Using temporary` | 使用了临时表解决查询 | 通常出现在 GROUP BY 或 DISTINCT,需要优化 | | `Using filesort` | 需要额外排序,无法利用索引顺序 | 考虑加索引消除排序 | | `Using index condition` | 索引下推(ICP),在存储引擎层过滤 | 好消息,说明 MySQL 自动优化了 | | `Using sort_union()` / `Using union()` | 使用了多个索引合并后再排序 | 考虑改造成单索引 | | `Impossible where` | WHERE 条件永远不成立 | 检查 SQL 逻辑 | | `Select tables optimized away` | 最小化优化,无需访问表 | 最优情况 | ```sql -- 看到 WARNING 时的排查思路 EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2025; -- Extra: Using where; Using filesort -- 问题:YEAR() 函数作用于字段 → 无法使用索引 -- 优化:改用范围查询 -- WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01' ``` ## 代码示例 ```sql -- 典型 EXPLAIN 输出解读 EXPLAIN SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 1 ORDER BY o.created_at DESC LIMIT 10; -- 解读要点: -- 第一行:u 表 → select_type=PRIMARY, type=ref (status 上有索引) -- → key=idx_status, rows≈500, filtered=100% -- 第二行:o 表 → type=ref (user_id 是外键索引), eq_ref -- → 对每个 u 的行,通过 user_id 精确匹配一条订单 -- Extra: Using index condition → ICP 生效 -- Using filesort → created_at 上没有合适索引,需额外排序 -- 优化方向:给 orders(created_at) 建索引,消除 filesort ``` ## 实践场景 **场景一:日常慢查询排查** 1. 开启慢查询日志(`long_query_time=1`) 2. 对慢 SQL 加 `EXPLAIN` 前缀执行 3. 检查 type 是否退化到 `ALL` 或 `index` 4. 关注 Extra 中的 `Using temporary` 和 `Using filesort` 5. 针对问题点调整索引或改写 SQL **场景二:判断索引是否被有效利用** ```sql -- 如果发现某张表的 key 列为 NULL,说明该查询完全没有使用索引 -- 检查 possible_keys 是否有可选索引但未被选中 -- 如果是,可能是统计信息过时,执行 ANALYZE TABLE ANALYZE TABLE orders; ``` **场景三:JOIN 查询优化** - 小表驱动大表:JOIN 中较小的表放在前面(EXPLAIN 靠上的表先被执行) - 确保 JOIN 条件两侧都有对应的索引 - 避免在多列上使用 OR 连接 JOIN 条件,这会迫使优化器退化为全表扫描 ## 扩展阅读 - [[B+树索引原理]] - [[覆盖索引与回表优化]] - [[ACID 与 MVCC 机制]]