面试官:MySQL 索引优化你只会加索引?Explain 执行计划的 12 个字段全解析
最近接触了几个面试,问到 "MySQL 怎么优化慢 SQL",回答基本就一句话:"加索引呗。" 再追问 "你怎么确定加哪个索引?Explain 的 type 字段有哪些取值?什么是 Using filesort?" 要么含糊其辞,要么干脆答不上来。
说实话,只会 EXPLAIN + SQL 然后盯着 key 字段看"有没有走索引",这不算会用 Explain。Explain 输出的 12 个字段里藏着优化器的全部决策细节:SQL 先执行哪一部分、联合索引用了几个字段、排序走没走索引、临时表有没有产生——这些才是判断 SQL 性能瓶颈的关键。
今天这篇文章,把 Explain 的 12 个字段拆开来逐个讲,文末再放一道真实的 SQL 优化案例,带大家把学到的字段串起来用一遍。
一、执行计划长什么样?先有个整体印象
在任意一条 SELECT/UPDATE/DELETE/INSERT 语句前加上 EXPLAIN,MySQL 不会真的执行 SQL,而是返回优化器评估后的"执行计划":
EXPLAIN SELECT e.emp_name, d.dept_name
FROM t_employee e
INNER JOIN t_department d ON e.dept_id = d.id
WHERE e.age > 30;
执行后你会看到一张表(以下是基于示例工程 1 万条员工数据的真实输出):
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | d | NULL | ALL | PRIMARY | NULL | NULL | NULL | 18 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | e | NULL | ref | idx_dept_id,idx_name_age_salary | idx_dept_id | 9 | explain_demo.d.id | 555 | 33.33 | Using where |
这 12 个字段的重要程度不同。我按照面试出现频率 + 实战中"需要重点看"的优先级,把它们分为 3 档:
- 🔥 必看(Top 6):
type、key、rows、Extra、key_len、id - ⭐ 次看(4 个):
select_type、possible_keys、filtered、ref - 💡 了解(2 个):
table、partitions
接下来我们从必看的 6 个开始,逐个拆解。
二、🔥 必看字段 Top 6
1️⃣ type:访问类型——性能分层的"温度计"
type 是 Explain 里最重要的一个字段,没有之一。它告诉我们 MySQL 是怎样从表里找行的,取值从差到好有 7 个层级:
ALL < index < range < ref < eq_ref < const/system < NULL
↑ 最差 最好 ↑
下面我们逐个看每个取值到底是什么场景、对应的 SQL 怎么写。
(1)ALL:全表扫描 —— 性能红线
含义:从头扫到尾,一行行找。数据量大了直接 OOM。
-- ❌ phone 列没有索引,必然 ALL
EXPLAIN SELECT * FROM t_employee WHERE phone = '13800000001';
输出(关键部分,下同):
| type | key | rows | Extra |
|---|---|---|---|
| ALL | NULL | 10000 | Using where |
判断时机:type = ALL 基本就两种原因——① 没加索引;② 索引失效(函数/隐式转换/前导% LIKE)。除了小表(< 1000 行)做全表比走索引更划算之外,其他场景必须修。
(2)index:全索引扫描 —— "比全表好一点但仍然慢"
含义:不读数据文件了,但把整个索引树遍历了一遍。常见于"查询的列刚好全在某个索引里"但没任何 WHERE 条件的场景。
-- 只有 emp_name 在联合索引 idx_name_age_salary 里,没有过滤条件
EXPLAIN SELECT emp_name FROM t_employee;
| type | key | rows | Extra |
|---|---|---|---|
| index | idx_name_age_salary | 10000 | Using index |
Using index 是覆盖索引的信号(后面 Extra 会详细讲),这里是好的;但 type = index 说明索引被全扫了,如果 WHERE 条件加不上,大表仍然很慢。
(3)range:索引范围扫描 —— 日常查询的主力
含义:在 B+ 树索引上按范围取行。触发条件:>、<、BETWEEN、IN、LIKE 'xxx%'(前缀匹配,前导 % 不走)。
EXPLAIN SELECT * FROM t_employee WHERE salary BETWEEN 20000 AND 30000;
EXPLAIN SELECT * FROM t_employee WHERE dept_id IN (6, 7, 8);
EXPLAIN SELECT * FROM t_employee WHERE emp_name LIKE '员工00%';
以 salary BETWEEN 为例:
| type | key | key_len | rows | Extra |
|---|---|---|---|---|
| range | idx_salary | 6 | 2138 | Using where |
注意:
IN (...)在 InnoDB 里是 range,不是 ref。值少和值多走的计划可能不同。
(4)ref:非唯一性索引的等值查询 —— 最常用的"正常级别"
含义:用普通索引(非唯一/非主键)做等值匹配,可能匹配多行。
EXPLAIN SELECT * FROM t_employee WHERE dept_id = 6;
| type | key | key_len | rows |
|---|---|---|---|
| ref | idx_dept_id | 9 | 555 |
dept_id 是普通索引,每个部门有几百人,所以返回多行,归类为 ref。这是日常业务查询最常见、也最"健康"的 type。
(5)eq_ref:唯一性索引等值 JOIN —— JOIN 场景的天花板
含义:在 JOIN 查询中,第二张表用主键或唯一键关联,最多只匹配一行。这是 JOIN 里性能最好的访问类型。
EXPLAIN
SELECT e.emp_name, d.dept_name
FROM t_employee e
INNER JOIN t_department d ON d.id = e.dept_id
WHERE e.salary > 30000;
| id | table | type | key | key_len | ref | rows |
|---|---|---|---|---|---|---|
| 1 | e | range | idx_salary | 6 | NULL | 1746 |
| 1 | d | eq_ref | PRIMARY | 8 | explain_demo.e.dept_id | 1 |
d.id 是 t_department 的主键,每个 e.dept_id 对应 最多 1 条 部门记录 → eq_ref。
区分 eq_ref / ref 小技巧:看被驱动表(第二张表)的关联字段是不是唯一的。主键/唯一键 → eq_ref;普通索引 → ref。
(6)const / system:主键/唯一键等值命中 —— 极致性能
含义:查询一开始就读取,整个生命周期里只取一行。system 是 const 的特例(表里只有 1 行,MyISAM 引擎常见)。
EXPLAIN SELECT * FROM t_employee WHERE id = 1001;
EXPLAIN SELECT * FROM t_employee WHERE emp_no = 'E000001';
EXPLAIN SELECT * FROM t_department WHERE dept_name = '后端开发部';
| type | key | key_len | ref | rows |
|---|---|---|---|---|
| const | PRIMARY | 8 | const | 1 |
| const | uk_emp_no | 130 | const | 1 |
| const | uk_dept_name | 258 | const | 1 |
面试常考:把 type 从差到好背下来,每个举一个例子。这道题我自己面试时问过不下 50 人,能完整答对的 < 10%。
2️⃣ key & possible_keys:可能走 vs 实际走
这两个字段放一起看,能回答一个经典问题:"我明明加了索引,为什么 SQL 还是慢?"
possible_keys:可能用到的索引(优化器先挑一批候选)key:优化器最后真正选的索引
两者不一致的时候,就是"为什么不走这个索引"的分析点。
典型场景 1:possible_keys 有值,但 key = NULL
-- status 列未建索引,只有 1/2/3 三种值,区分度极低
EXPLAIN SELECT * FROM t_employee WHERE status = 1;
| possible_keys | key | type | rows |
|---|---|---|---|
| NULL | NULL | ALL | 10000 |
但如果你提前给 status 加了个单列索引(我们示例里没加),你会看到:
| possible_keys | key | type | rows |
|---|---|---|---|
| idx_status | NULL | ALL | 10000 |
这就是典型的 "有索引但不用"——优化器估算走索引回表的成本比全表还高,干脆直接扫。
经验阈值:单列索引区分度 < 10% 基本不会走。像
status、gender、is_deleted这种只有 2~5 个取值的列,单独建索引几乎永远用不上,必须和区分度高的列组成联合索引。
典型场景 2:possible_keys 多个,优化器挑了"意料之外"的
-- 两个候选索引:idx_dept_id / idx_salary
EXPLAIN SELECT * FROM t_employee
WHERE dept_id = 6 AND salary BETWEEN 20000 AND 40000;
| possible_keys | key | type | rows |
|---|---|---|---|
| idx_dept_id, idx_salary | idx_dept_id | ref | 555 |
为什么挑 idx_dept_id 不挑 idx_salary?因为优化器算成本:dept_id = 6 大概 555 行,再里面过滤 salary;而 salary BETWEEN 大概 3000+ 行再过滤 dept_id。前者更便宜。
强制验证工具:
FORCE INDEX(xxx)可以强制走某个索引,用来对比真实耗时判断优化器决策是否合理。生产环境不要硬编码 FORCE INDEX,版本升级/数据变化后可能反优化。
3️⃣ key_len:联合索引用了几个字段?
这是 Explain 里最被低估的字段。面试答上来直接加分。
key_len 表示索引使用的字节数。通过这个数字可以反向推断联合索引的前缀几个字段真正被用到了。
字节数计算规则(utf8mb4 字符集)
| 字段类型 | 基础字节 | 变长额外 | 允许 NULL 额外 | 合计示例 |
|---|---|---|---|---|
| VARCHAR(n) | n × 4 | +2 | +1 | VARCHAR(64) + NULL = 64×4+2+1 = 259 |
| CHAR(n) | n × 4 | 0 | +1 | CHAR(10) + NULL = 41 |
| INT | 4 | 0 | +1 | 5 |
| BIGINT | 8 | 0 | +1 | 9 |
| TINYINT | 1 | 0 | +1 | 2 |
| DATE | 3 | 0 | +1 | 4 |
| DECIMAL(m,d) | 约⌈m/2⌉(压缩存储) | 0 | +1 | DECIMAL(10,2) ≈ 6 |
核心记忆点:utf8mb4 每个字符占 4 字节,变长 +2,允许 NULL +1。所以定长字段尽量设为 NOT NULL,省字节也省判断。
示例:联合索引 idx_name_age_salary(emp_name, age, salary)
我们一步步看最左前缀原则的实际效果:
① 只用到第一个字段 emp_name(key_len = 259)
EXPLAIN SELECT * FROM t_employee WHERE emp_name = '员工01234';
| possible_keys | key | key_len |
|---|---|---|
| idx_name_age_salary,... | idx_name_age_salary | 259 |
② 用到前两个字段 emp_name + age(key_len = 259 + 2 = 261)
EXPLAIN SELECT * FROM t_employee
WHERE emp_name = '员工01234' AND age = 30;
| key_len |
|---|
| 261 |
③ 三个字段全用到(key_len = 259 + 2 + 6 = 267)
EXPLAIN SELECT * FROM t_employee
WHERE emp_name = '员工01234' AND age = 30 AND salary = 25000.00;
| key_len |
|---|
| 267 |
④ 违反最左前缀,跳过 age,只用了 emp_name(key_len 还是 259)
EXPLAIN SELECT * FROM t_employee
WHERE emp_name = '员工01234' AND salary = 25000.00;
| key_len |
|---|
| 259 |
面试题:联合索引 (a, b, c),WHERE a = ? AND c = ?,key_len 是多少?
答:只有 a 的长度。因为跳过了 b,c 用不上(ICP 会让 c 在索引上过滤,但不算"被 key_len 包含",这点区分好)。
4️⃣ rows:估算要扫多少行?
rows 是优化器根据统计信息估算要读取的行数,不是精确值,但足够用来做量级判断。
核心原则:rows × 每张表 = 整个查询的复杂度上限。
举个递进的例子(员工表 1 万行):
| SQL 条件 | type | rows |
|---|---|---|
| WHERE dept_id = 6 | ref | 555 |
| WHERE dept_id = 6 AND age > 25 | ref | 555 |
| WHERE dept_id = 6 AND age > 25 AND salary > 30000 | ref | 555 |
注意到后两行的 rows 也是 555?因为优化器只按索引列 dept_id 估算,age/salary 的过滤效果体现在 filtered 字段上(下面就讲)。
排查小技巧:如果 rows 跟你实际 LIMIT 出来的数量差 10 倍以上,八成是统计信息过期了,跑一下
ANALYZE TABLE t_employee;更新直方图。
5️⃣ id:SQL 先执行哪一块?
id 是查询的"执行顺序号",有三条规则:
- id 相同 → 从上到下顺序执行
- id 不同 → id 越大越先执行(子查询优先)
- id 有 NULL → 最后执行(UNION 的合并结果)
来看三个真实例子。
示例 1:id 相同,从上到下
SELECT e.emp_name, d.dept_name
FROM t_employee e INNER JOIN t_department d ON e.dept_id = d.id
WHERE e.age > 30;
| id | table | type |
|---|---|---|
| 1 | d | ALL |
| 1 | e | ref |
两行 id 都是 1,所以 MySQL 先扫 t_department(d),再用 dept_id 去关联 t_employee(e)。顺序就是 d → e。
示例 2:id 不同,子查询先跑
SELECT emp_name, salary
FROM t_employee
WHERE dept_id = (SELECT id FROM t_department WHERE dept_name = '后端开发部');
| id | select_type | table | type |
|---|---|---|---|
| 2 | SUBQUERY | t_department | const |
| 1 | PRIMARY | t_employee | ref |
id=2 是子查询,先拿到"后端开发部"的 id=6,再用 id=1 的 PRIMARY 查询员工。
示例 3:UNION 最后一行 id=NULL
SELECT id, emp_name FROM t_employee WHERE dept_id = 6
UNION
SELECT id, emp_name FROM t_employee WHERE salary > 50000;
| id | select_type | table |
|---|---|---|
| 1 | PRIMARY | t_employee |
| 2 | UNION | t_employee |
| NULL | UNION RESULT |
UNION 结果行 id 为 NULL,是最后做去重合并的那一步。
6️⃣ Extra:信息量最大的"附加说明"
Extra 是 Explain 里字段最短、信息最密的一列。优化 SQL 的 80% 工作就是处理这一列里的"红色预警"。
我把 Extra 的常见取值分成三类:❌ 危险信号 / ⚠️ 正常信号 / ✅ 优秀信号。
❌ 危险信号(必须优化)
Using filesort:外部排序,没走索引排序
含义:ORDER BY / GROUP BY 的列没命中索引,MySQL 需要在内存/磁盘里做额外排序。
-- 没有 (dept_id, salary) 联合索引
EXPLAIN SELECT * FROM t_employee WHERE dept_id = 6 ORDER BY salary DESC;
| type | key | Extra |
|---|---|---|
| ref | idx_dept_id | Using where; Using filesort |
这里 Using filesort 跟"文件"没关系,只是叫这名。数据量小在内存里排,大了就落临时文件,性能断崖。
修复方法:给 WHERE 条件列 + ORDER BY 列建联合索引,让 ORDER BY 直接走索引有序性:
ALTER TABLE t_employee ADD KEY idx_dept_salary (dept_id, salary);
-- 再 EXPLAIN 一次
| type | key | Extra |
|---|---|---|
| ref | idx_dept_salary | Using where |
Using filesort 消失了!因为拿到的行本身就按 salary 有序,直接返回即可。
Using temporary:使用了临时表
含义:GROUP BY / DISTINCT / UNION 过程中,MySQL 建了一张内部临时表放中间结果。
EXPLAIN
SELECT dept_id, AVG(salary) AS avg_s
FROM t_employee
WHERE age > 30
GROUP BY dept_id;
| type | key | Extra |
|---|---|---|
| ALL | NULL | Using where; Using temporary; Using filesort |
没有 (age, dept_id) 索引时,先按 age 过滤后要按 dept_id 分组,只能建临时表 + 对分组结果排序,两个红灯同时亮。
修复思路:GROUP BY 的列加联合索引前缀;或者业务允许的话加 ORDER BY NULL 取消分组后的排序(只消除 filesort,不消除 temporary)。
✅ 优秀信号(越多越好)
Using index:覆盖索引,不需要回表
含义:SELECT 涉及的所有列刚好都在同一个索引里,不需要回表查聚簇索引的完整行。这是覆盖索引的直接证据。
-- 索引 idx_name_age_salary(emp_name, age, salary) 里包含了所有 SELECT 的列
EXPLAIN SELECT emp_name, age, salary FROM t_employee
WHERE emp_name = '员工01234' AND age = 30;
| type | key | Extra |
|---|---|---|
| ref | idx_name_age_salary | Using index |
Using index + type = ref,是单表查询里的理想组合。优化原则:能用覆盖索引就别 SELECT *。
Using index condition:索引条件下推(ICP)
含义:MySQL 5.6 引入的优化。本来索引只用来定位行、其他条件回 Server 层再过滤;ICP 开启后,索引里能判断的条件直接在存储引擎层过滤完,减少回表和传上 Server 层的行数。
EXPLAIN SELECT * FROM t_employee
WHERE emp_name = '员工01234' AND age > 35;
| Extra |
|---|
| Using index condition; Using where |
看到 Using index condition 就意味着:emp_name 用来 B+ 树定位,age(同样在联合索引里)直接在引擎层判断 > 35,过滤掉不合格的再回表,回表次数大大降低。
ICP 默认开启(
optimizer_switch = 'index_condition_pushdown=on'),一般不用手动关。
Select tables optimized away:极致优化
含义:MIN/MAX 直接取 B+ 树的最左/最右叶节点,不用扫描行。
EXPLAIN SELECT MIN(salary), MAX(salary) FROM t_employee;
| type | key | rows | Extra |
|---|---|---|---|
| NULL | NULL | NULL | Select tables optimized away |
这个信号只出现在对索引列做 MIN/MAX 且无 GROUP BY的场景,已经是理论最优。
⚠️ 正常信号(不必过度反应)
Using where
Server 层拿到存储引擎返回的行之后,又在 Service 层用 WHERE 条件过滤了一遍。常见于索引没有覆盖所有过滤条件的场景,不代表有问题,但可以考虑加联合索引进一步优化。
Impossible WHERE
EXPLAIN SELECT * FROM t_employee WHERE 1 = 2;
| type | rows | Extra |
|---|---|---|
| NULL | NULL | Impossible WHERE |
WHERE 条件永远为假,MySQL 直接返回空,不会读表。这种写得离谱的 SQL 线上少见,但排查时看到了别慌,是语法逻辑层面的问题。
三、⭐ 次看 4 字段
7️⃣ select_type:这条 SELECT 是什么角色?
告诉我们当前行属于查询里的哪个部分,常见 6 个取值:
| 取值 | 含义 | 出现场景 |
|---|---|---|
| SIMPLE | 简单查询 | 无 UNION / 子查询的普通 SELECT |
| PRIMARY | 主查询 | 包含子查询时的最外层 SELECT |
| SUBQUERY | 非相关子查询 | SELECT / WHERE 里的子查询(独立执行一次) |
| DEPENDENT SUBQUERY | 相关子查询 | 子查询依赖外层列,每行都要执行一次,慢 |
| UNION | UNION 的后续 SELECT | UNION 里第二个及以后的查询 |
| DERIVED | 派生表 | FROM (SELECT ...) t,MySQL 5.7+ 会尝试合并 |
重点提一下 DEPENDENT SUBQUERY,性能杀手级的存在:
EXPLAIN
SELECT e.emp_name,
(SELECT dept_name FROM t_department d WHERE d.id = e.dept_id) AS dept_name
FROM t_employee e
WHERE e.age > 40;
| id | select_type | table |
|---|---|---|
| 1 | PRIMARY | e |
| 2 | DEPENDENT SUBQUERY | d |
子查询里用到了外层的 e.dept_id,意味着外层每返回一行员工,子查询就要执行一次。如果外层匹配了 1000 个员工,子查询被调 1000 次。改写成 JOIN 通常能让优化器选出更优的 NL/HASH JOIN 计划。
8️⃣ filtered:过滤后剩余行数占比
rows × filtered / 100 就是优化器预估要传给下一张表 JOIN 的行数。这个值越接近 100 越好,说明 WHERE 条件对当前索引定位出来的行过滤得很精准。
-- 高过滤:emp_name LIKE '员工00%' 定位到 100 行,几乎都保留
EXPLAIN SELECT * FROM t_employee WHERE emp_name LIKE '员工00%'; -- filtered ≈ 100%
-- 低过滤:全表扫,WHERE phone LIKE '%138%' 在 Server 层才过滤
EXPLAIN SELECT * FROM t_employee WHERE phone LIKE '%138%'; -- filtered ≈ 11.11%
filtered 低 + type = ALL 组合出现时,基本就意味着"该加索引了"。
9️⃣ ref:索引跟谁比?
ref 显示的是索引列被用来和什么比较。常见三种:
| ref 值 | 含义 |
|---|---|
| const | 与常量比较(WHERE emp_no = 'E000001') |
| db.table.col | JOIN 时与另一张表的列比较(d.id = e.dept_id) |
| func | 与某个函数值比较(少用,出现了通常意味着写得怪) |
🔟 table:正在查哪张表?
大部分情况就是表名/别名。两种特殊值:
<derivedN>:FROM 里的派生表,N 对应子查询的 id<unionM,N>:UNION 合并的临时表,M/N 对应参与 UNION 的 id
四、💡 了解 2 字段
1️⃣1️⃣ partitions:命中的分区
分区表才会有值。绝大多数没做分区的线上表都是 NULL。
1️⃣2️⃣ partitions / table:其他
剩下就没什么好展开的了,遇到再说。
五、综合实战:一条慢 SQL 的优化前后对比
光说不练假把式。我们把上面的知识点用在一道真实的统计 SQL 上,一步一步把它从"跑几秒"优化到"毫秒级"。
场景
统计 2023 年入职的在职员工,按部门聚合出人数、平均薪资、最高薪资,按平均薪资倒序取前 10。
优化前 SQL
SELECT d.dept_name,
COUNT(*) AS emp_count,
AVG(e.salary) AS avg_salary,
MAX(e.salary) AS max_salary
FROM t_department d
LEFT JOIN t_employee e ON e.dept_id = d.id
WHERE e.status = 1
AND YEAR(e.hire_date) = 2023 -- ⚠️ 对索引列做函数
GROUP BY d.dept_name
ORDER BY avg_salary DESC
LIMIT 10;
执行计划输出(节选):
| id | table | type | key | rows | Extra |
|---|---|---|---|---|---|
| 1 | d | ALL | NULL | 18 | Using temporary; Using filesort |
| 1 | e | ALL | NULL | 10000 | Using where; Using join buffer (hash join) |
问题诊断(把前面学的字段一个个对上):
- type = ALL(两张表都是):全表扫,员工表扫 10000 行
- Using temporary:按 dept_name 分组建了临时表
- Using filesort:ORDER BY avg_salary 在临时表上额外排序
- YEAR(e.hire_date):对 hire_date 做函数运算,即使有 idx_hire_date 也不会走
- e.status 区分度低:单列加索引也白搭
优化方案
第一步:改写法——去掉索引列上的函数
-- YEAR(e.hire_date) = 2023
-- ↓ 等价替换
AND e.hire_date BETWEEN '2023-01-01' AND '2023-12-31'
第二步:建覆盖联合索引——把 WHERE + JOIN 用到的列打包成一条索引
ALTER TABLE t_employee
ADD KEY idx_status_hire_dept_salary (status, hire_date, dept_id, salary);
为什么这么排?遵循联合索引的"等值在前,范围在后,包含 SELECT 列"的原则:
status = 1:等值 → 放第一位hire_date BETWEEN ...:范围 → 放第二位dept_id:JOIN 关联列 → 放第三位salary:聚合用到 AVG/MAX,放进来让它变成覆盖索引,不回表
优化后 SQL
SELECT d.dept_name,
COUNT(*) AS emp_count,
AVG(e.salary) AS avg_salary,
MAX(e.salary) AS max_salary
FROM t_department d
LEFT JOIN t_employee e ON e.dept_id = d.id
WHERE e.status = 1
AND e.hire_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY d.dept_name
ORDER BY avg_salary DESC
LIMIT 10;
优化后执行计划:
| id | table | type | key | rows | Extra |
|---|---|---|---|---|---|
| 1 | d | ALL | PRIMARY | 18 | Using temporary; Using filesort |
| 1 | e | ref | idx_status_hire_dept_salary | 210 | Using index |
关键改善点:
| 指标 | 优化前 | 优化后 | 效果 |
|---|---|---|---|
| 员工表 type | ALL | ref | 全表 → 走索引 |
| 员工表 rows | 10000 | 210 | 扫描行数降到 1/48 |
| 员工表 Extra | Using where; join buffer | Using index | 覆盖索引,零回表 |
| GROUP BY avg_salary 临时表 | 仍有(聚合计算天然需要) | 仍有 | 但输入行数少了两个数量级,代价可忽略 |
说明:
ORDER BY avg_salary因为排序对象是聚合后的 18 行部门数据,Using temporary + Using filesort完全在可接受范围内。不要盲目追求 Extra 里没有任何警告,要结合量级判断。
六、面试高频话术整理
最后给大家整理一段遇到 Explain 相关面试题时的回答模板,照着说基本不会出错:
"Explain 输出我主要看 6 个字段:type、key、key_len、rows、Extra、id。
首先看 type,如果是 ALL 就要想办法加索引或改写,目标是至少到 range,正常情况 ref;主键 JOIN 能到 eq_ref,主键等值查询能到 const。
然后看 key 和 possible_keys,确认候选里是否有合适的索引、优化器为什么没选;key_len 用来推算联合索引前缀用了几个字段,检查是否违反最左前缀。
rows 是估算扫描行数,量级对不对,和 filtered 乘一下判断传给下游的数量。
Extra 是重点:Using filesort 就给 ORDER BY 加联合索引;Using temporary 就考虑覆盖索引或改写 GROUP BY;看到 Using index 就说明覆盖索引生效。
最后用 id 判断执行顺序:同号从上到下,异号大的先跑,id=NULL 是 UNION 合并。"
这段话术涵盖了 80% 的面试追问,剩下的 20% 就是考你有没有真实优化经验——把今天的综合案例说清楚,面试官基本就点头了。
七、总结 & 配套资源
核心速记口诀
type 层级别记混:ALL→index→range→ref→eq_ref→const
key_len 用处大:联合索引用几段,算字节就知道
Extra 三个红灯:filesort/temporary 要干掉,Using index 多多益善
possible_keys 是候选,key 才是实锤,不用就分析成本
建议大家本地 MySQL 跑一遍,把每个 EXPLAIN 输出和本文的讲解一一对应,比死记硬背有效 10 倍。
🗨️ 互动话题
你在项目里遇到过 Explain 里最"离谱"的计划是什么? 欢迎在评论区留言分享:
- 有没有见过
possible_keys一堆但key为 NULL 的反直觉情况?最后怎么解决的? - 你有没有靠
key_len发现联合索引用错字段的真实经历? - 对于 MySQL 8.0 新出的
EXPLAIN ANALYZE(真执行 + 实际耗时),你用了吗?体验如何?
本公众号专注 Java / 分布式 / MySQL / 微服务。更多深度文章欢迎订阅公众号「服务端技术精选」或访问我的博客。
标题:面试官:MySQL 索引优化你只会加索引?Explain 执行计划的 12 个字段全解析
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/08/22/1787389990474.html
公众号:服务端技术精选
- 一、执行计划长什么样?先有个整体印象
- 二、🔥 必看字段 Top 6
- 1️⃣ type:访问类型——性能分层的"温度计"
- (1)ALL:全表扫描 —— 性能红线
- (2)index:全索引扫描 —— "比全表好一点但仍然慢"
- (3)range:索引范围扫描 —— 日常查询的主力
- (4)ref:非唯一性索引的等值查询 —— 最常用的"正常级别"
- (5)eq_ref:唯一性索引等值 JOIN —— JOIN 场景的天花板
- (6)const / system:主键/唯一键等值命中 —— 极致性能
- 2️⃣ key & possible_keys:可能走 vs 实际走
- 典型场景 1:possible_keys 有值,但 key = NULL
- 典型场景 2:possible_keys 多个,优化器挑了"意料之外"的
- 3️⃣ key_len:联合索引用了几个字段?
- 字节数计算规则(utf8mb4 字符集)
- 示例:联合索引 idx_name_age_salary(emp_name, age, salary)
- 4️⃣ rows:估算要扫多少行?
- 5️⃣ id:SQL 先执行哪一块?
- 示例 1:id 相同,从上到下
- 示例 2:id 不同,子查询先跑
- 示例 3:UNION 最后一行 id=NULL
- 6️⃣ Extra:信息量最大的"附加说明"
- ❌ 危险信号(必须优化)
- Using filesort:外部排序,没走索引排序
- Using temporary:使用了临时表
- ✅ 优秀信号(越多越好)
- Using index:覆盖索引,不需要回表
- Using index condition:索引条件下推(ICP)
- Select tables optimized away:极致优化
- ⚠️ 正常信号(不必过度反应)
- Using where
- Impossible WHERE
- 三、⭐ 次看 4 字段
- 7️⃣ select_type:这条 SELECT 是什么角色?
- 8️⃣ filtered:过滤后剩余行数占比
- 9️⃣ ref:索引跟谁比?
- 🔟 table:正在查哪张表?
- 四、💡 了解 2 字段
- 1️⃣1️⃣ partitions:命中的分区
- 1️⃣2️⃣ partitions / table:其他
- 五、综合实战:一条慢 SQL 的优化前后对比
- 场景
- 优化前 SQL
- 优化方案
- 优化后 SQL
- 六、面试高频话术整理
- 七、总结 & 配套资源
- 核心速记口诀
- 🗨️ 互动话题
评论