从数据结构 · 生成流程 · EXPLAIN字段 · 性能诊断 · 优化实战 — 一图搞懂
执行计划在 MySQL 内部是一棵 查询树(Query Tree),每个节点是一个"操作算子",叶子节点是表/索引访问,父节点是 JOIN/聚合/排序等操作。
MySQL 内部用 SELECT_LEX(8.0 重命名为 Query_block)表示每个查询块,JOIN 操作封装在 JOIN 对象里,每张表对应一个 JOIN_TAB:
// 简化示意(MySQL 源码 sql/sql_optimizer.h) struct JOIN_TAB { TABLE *table; // 对应的表对象 Key_use *keyuse; // 可用的索引访问方式列表 ha_rows records; // 估计返回行数(统计信息) double read_time; // 读取代价估算 enum join_type type; // ALL / ref / range / eq_ref... Item *condition; // WHERE 过滤条件 uint use_quick; // 是否使用 Range 优化 }; struct JOIN { JOIN_TAB *join_tab; // 所有表的 JOIN_TAB 数组 uint tables; // 参与 JOIN 的表数量 double best_read; // 当前最优计划代价 JOIN_TAB *best_ref[]; // 最优连接顺序 ha_rows best_rowcount; // 预计总行数 };
-- 全表扫描代价(简化) Total_cost = pages × 1.0 /* IO:读取数据页数 */ + rows × 0.2 /* CPU:处理每行代价 */ -- 索引范围扫描代价 Total_cost = range_rows / records_per_key × 1.0 /* 索引IO */ + range_rows × 1.0 /* 回表IO(如需) */ + range_rows × 0.2 /* CPU */ -- 查看 MySQL 内置代价常量 SELECT * FROM mysql.server_cost; SELECT * FROM mysql.engine_cost;
EXPLAIN SELECT o.id, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'paid' AND u.city = 'Shanghai';
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | o | range | idx_status | idx_status | 4 | const | 1250 | 100.00 | Using index condition |
| 1 | SIMPLE | u | eq_ref | PRIMARY,idx_city | PRIMARY | 4 | o.user_id | 1 | 10.00 | Using where |
| 字段 | 含义 | 关键值 / 说明 |
|---|---|---|
id |
查询序号 | 相同 id 表示同一层,id 越大越先执行(子查询)。NULL 表示 UNION 结果合并行 |
select_type |
查询类型 | SIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION / DEPENDENT SUBQUERY... |
table |
当前行操作的表 | <derived N> 表示派生表,<union M,N> 表示 UNION 结果 |
partitions |
匹配的分区 | 仅分区表有效,NULL 表示未分区 |
type |
访问类型(性能关键) | system > const > eq_ref > ref > range > index > ALL ↓ 性能递减,见 §5 |
possible_keys |
候选索引列表 | 优化器考虑过的所有索引,不一定实际使用 |
key |
实际选择的索引 | NULL = 未用索引(警惕!),覆盖索引时 key 可能出现在 possible_keys 之外 |
key_len |
索引使用的字节数 | 可推算出用了几列。INT=4B,VARCHAR(N) UTF8MB4 = N×4+2B |
ref |
与索引比较的值 | const(常量)/ 表名.列名(关联列) |
rows |
预估扫描行数 | 基于统计信息的估算,不是精确值。乘以所有表的 rows ≈ 总工作量 |
filtered |
WHERE 过滤后剩余比例 | rows × filtered% = 实际传给下一步的行数。越低越好 |
Extra |
附加执行信息 | Using filesort / Using temporary 是性能警告,见 §6 |
type 字段是判断执行计划质量最关键的指标,从上到下性能递减:
range,核心查询要求 ref 或以上,ALL 必须排查!
| Extra 值 | 含义 | 说明 |
|---|---|---|
Using index |
覆盖索引 | 查询所需列都在索引中,无需回表访问数据行 ✅ 最佳状态 |
Using index condition |
索引条件下推(ICP) | MySQL 5.6+,WHERE 条件在索引层过滤,减少回表次数 |
Using where; Using index |
覆盖索引 + WHERE 过滤 | 有额外 WHERE 过滤,但仍只读索引,无回表 |
const row not found |
const 扫描无结果 | 说明常量查找命中索引但结果为空,不影响性能 |
| Extra 值 | 含义 | 建议 |
|---|---|---|
Using where |
Server 层 WHERE 过滤 | 索引扫描后还需在 server 层过滤,可能意味着索引不够精准 |
Using join buffer |
使用 Join Buffer | 被驱动表没有用索引,用内存 buffer 做 BNL join,考虑加索引 |
Not exists |
LEFT JOIN 优化 | LEFT JOIN 被优化为"找到一行即停止",通常无需处理 |
Select tables optimized away |
聚合被优化掉 | MIN/MAX 直接读索引极值,非常快 |
| Extra 值 | 含义 | 危害 | 解决方案 |
|---|---|---|---|
Using filesort |
文件排序(内存或磁盘) | 无法通过索引顺序满足 ORDER BY,需额外排序,大数据量慢 | 建复合索引覆盖 WHERE + ORDER BY 列 |
Using temporary |
使用临时表 | GROUP BY / DISTINCT 无法用索引,建内存/磁盘临时表,极耗资源 | 确保 GROUP BY 列有索引,或调整查询 |
Using filesort + Using temporary |
同时排序 + 临时表 | 双重开销,大表查询极危险 | 重构查询,拆分聚合,加合适索引 |
Impossible WHERE |
WHERE 矛盾 | WHERE 1=0 等永假条件,不执行查询,但说明 SQL 有逻辑问题 | 检查查询逻辑 |
是否有 ALL(全表扫描)?是否有 index(全索引扫描)?目标:range 及以上
是否为 NULL?若 possible_keys 非空但 key 是 NULL,优化器认为全扫更快,说明索引基数低或统计信息偏差
rows 越小越好,filtered 越大越好(意味着 WHERE 更精准)。rows 过大说明索引选择性差
发现 Using filesort 或 Using temporary?立即排查 ORDER BY / GROUP BY 的索引覆盖情况
EXPLAIN ANALYZE 会真正执行 SQL 并返回实际执行时间和行数,对比估算值发现统计信息偏差
-- 1. 基础执行计划 EXPLAIN SELECT * FROM orders WHERE status = 'paid'; -- 2. JSON 格式(包含代价数值,MySQL 5.7+) EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE status = 'paid'; -- 3. 实际执行分析(MySQL 8.0.18+) EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'paid'; -- 4. 查看优化器开关 SELECT @@optimizer_switch; -- 5. 强制使用 / 忽略某个索引 SELECT * FROM orders FORCE INDEX (idx_status) WHERE status = 'paid'; SELECT * FROM orders IGNORE INDEX (idx_status) WHERE status = 'paid'; -- 6. 查看索引统计信息 SHOW INDEX FROM orders; ANALYZE TABLE orders; -- 重新收集统计信息