为什么写这篇:EXPLAIN 是 MySQL 调优的"听诊器",但官方文档对
key_len、filtered、Extra这些字段解释过于简略。本文用真实 SQL 示例把每个字段的含义讲透,并配 9 道 SQL 笔试题的标准解法——这些题在笔试/面试中重复率极高。适用读者:需要做 SQL 优化的后端、面试准备、想系统性梳理 SQL 语法的同学。
前置知识:会基本 SQL,了解索引概念。
目录
1. EXPLAIN 基础:一个完整案例
| |
第一次 EXPLAIN 输出:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | base_log | ALL | 1320000 | 10.00 | Using where; Using filesort |
type=ALL是全表扫描。rows=1320000表示扫描 132 万行。Using filesort表示额外排序。
优化一:加索引
| |
第二次 EXPLAIN 输出:
| id | select_type | table | type | key | key_len | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | base_log | index | idx_OperateTime | 8 | 15 | Using where; Backward index scan |
type从ALL升级到index(全索引扫描)rows从 132 万降到 15(只用 LIMIT 15 即可)Extra出现Backward index scan(对F_OperateTime倒序扫,命中ORDER BY DESC)
结论:单列索引 + LIMIT 优化,让 132 万行扫描变成 15 行扫描,性能从秒级降到毫秒级。
2. EXPLAIN 字段逐列解读
| 字段 | 含义 |
|---|---|
id | SELECT 标识符。同一个 SELECT 共享 id;子查询/UNION 会递增 |
select_type | 查询类型(SIMPLE / PRIMARY / UNION / SUBQUERY / DEPENDENT SUBQUERY) |
table | 当前行访问的表 |
partitions | 匹配的分区(如果表是分区表) |
type | 最重要:join 类型,决定性能等级 |
possible_keys | 优化器考虑使用的索引(不一定会用) |
key | 实际使用的索引 |
key_len | 索引使用的字节数(可判断复合索引用了几个字段) |
ref | 哪个字段或常数与 key 一起被使用 |
rows | 优化器估算要扫描的行数(实际可能偏差巨大) |
filtered | 过滤后剩余行数百分比 |
Extra | 额外信息(Using filesort / Using temporary / Using index 等) |
2.1 select_type 常见取值
SIMPLE:不含 UNION 和子查询的最简单查询PRIMARY:最外层 SELECTUNION:UNION 的第二或随后的查询SUBQUERY:子查询中的第一个 SELECTDEPENDENT SUBQUERY:依赖外层查询结果的子查询(通常很慢)
3. type 性能等级排序
从最差到最好:
| |
| type | 含义 | 性能 |
|---|---|---|
system | 表中只有 1 条数据 | 最佳 |
const | 主键/唯一索引等值扫描,最多 1 行 | 极佳 |
eq_ref | 多表 JOIN,每个前表结果只匹配后表 1 行 | 很好 |
ref | 非唯一索引等值扫描,或最左前缀命中 | 好 |
range | 索引范围扫描(=、<、>、BETWEEN、IN) | 中 |
index | 全索引扫描(不读数据文件) | 较差 |
ALL | 全表扫描 | 最差 |
底线:你的 SQL 不应该出现
type=ALL——除非表只有几十行,或者确实没有合适的过滤条件。
4. key_len 的字节数计算规则
key_len 能告诉你复合索引里实际用到了几个字段。
4.1 基础计算公式
字符串:
char(n):n字节varchar(n):utf8编码:3 * n + 2字节(2 是长度前缀)utf8mb4编码:4 * n + 2字节
数值:
| 类型 | 字节 |
|---|---|
| TINYINT | 1 |
| SMALLINT | 2 |
| MEDIUMINT | 3 |
| INT | 4 |
| BIGINT | 8 |
时间:
| 类型 | 字节 |
|---|---|
| DATE | 3 |
| TIMESTAMP | 4 |
| DATETIME | 8 |
字段属性:如果字段允许 NULL,多算 1 字节。
4.2 实战算例
| |
索引长度计算:
name varchar(50) utf8mb4:50 * 4 + 2 + 1 = 203(含 NULL 标志)age int NOT NULL:4dept varchar(20) utf8mb4:20 * 4 + 2 + 1 = 83
| SQL 条件 | 命中索引 | key_len |
|---|---|---|
WHERE name = 'a' | name | 203 |
WHERE name = 'a' AND age = 10 | name + age | 207 |
WHERE name = 'a' AND age = 10 AND dept = 'b' | 全部 | 290 |
WHERE age = 10 | 无 | NULL(不满足最左前缀) |
关键洞察:
key_len越大说明用到的索引字段越多。如果 key_len 跟你期望的不一致,往往是漏写了等值条件或写错了列顺序。
5. Extra 常见提示与优化方向
| Extra | 含义 | 优化方向 |
|---|---|---|
Using filesort | 额外的排序操作(不能用索引顺序) | 让 ORDER BY 命中复合索引顺序 |
Using temporary | 使用了临时表(GROUP BY / DISTINCT) | 让 GROUP BY 命中索引 |
Using index | 覆盖索引:只在索引里取数据,不回表 | 目标 |
Backward index scan | 倒序索引扫描 | 通常对 ORDER BY DESC 是好事 |
Using where | 存在 WHERE 子句筛选 | 配合 type 看 |
Using index condition | 索引下推(ICP,5.6+) | 通常是好事 |
Impossible WHERE | WHERE 永远为 false | 删掉这种 SQL |
6. 9 道经典 SQL 笔试题
表结构(学生选课经典案例):
| |
6.1 查重名学生
| |
关系型数据库查重复行的通用套路:
GROUP BY + HAVING COUNT > 1+ 子查询。
6.2 平均分不及格的学生
| |
WHERE过滤记录,HAVING过滤分组。聚集函数(AVG/COUNT/SUM)只能放HAVING里。
6.3 每门课都不低于 80 分的学生
| |
反向思维:要求"所有都 >= 80"等价于"不存在 < 80 的"。这是数理逻辑里的
∀x:P ≡ ¬∃x:¬P。
6.4 每个学生的总成绩(含没成绩的)
| |
INNER JOIN会过滤掉没成绩的学生。用LEFT JOIN才能保留。
6.5 总成绩最高的学生
| |
6.6 第 N 高的分数
| |
常见错误:
LIMIT 2然后MIN(score)——因为执行顺序是GROUP BY → MIN → ORDER BY → LIMIT,MIN 后 ORDER BY 已经无意义。正确做法是用
LIMIT offset, count跳过的语法。
6.7 每门课最高分的学生(核心)
| |
GROUP BY后的 SELECT 字段只能是:① GROUP BY 字段;② 聚集函数;③ 每个分组内均相同的字段。MySQL 的扩展语法(sid)默认开了ONLY_FULL_GROUP_BY是会拒绝的。
6.8 每门课的前 N 名
| |
核心思想:对于某个分数,如果同课程下比它高的分数少于 N 个,那么它就是前 N 名之一。
6.9 比赛两两组合
| |
自连接 + 不等式:本质是
team表和自己做笛卡尔积,再过滤掉a=b和a>b的重复。
