前言
慢查询优化面试题:
线上接口突然变慢,你定位到一条 SQL 慢查询,怎么优化?
90% 的人回答:加索引。
加索引确实是最常见手段,但追加了 3 个问题就卡壳:
- EXPLAIN 的 type 字段哪个值代表全表扫描? ------ 全 12 个字段你能看懂几个
- 索引加了还是慢,怎么办? ------ 不只是索引问题
- SQL 改了一行执行计划全变了,为什么? ------ 优化器做了什么
这篇文章从 EXPLAIN 到索引重建,把慢查询优化 10 招讲透。
一、EXPLAIN 12 个字段
慢查询第一步永远是 EXPLAIN。看懂这 12 个字段,你才算懂优化。
| 字段 | 含义 | 重点关注 |
|---|---|---|
| id | 查询序号 | id 越大越先执行;同 id 从上往下 |
| select_type | 查询类型 | SIMPLE/PRIMARY/SUBQUERY |
| table | 表名 | 哪张表慢 |
| type | 访问类型 | ⭐ 最重要,下面详解 |
| possible_keys | 可能用到的索引 | 为空 = 没有可用索引 |
| key | 实际用到的索引 | NULL = 没走索引 |
| key_len | 索引使用长度 | 判断联合索引用了几个列 |
| ref | 索引比较的列 | const/列名 |
| rows | 预估扫描行数 | ⭐ 越小越好 |
| filtered | 过滤后剩余比例 | 越大越好(100% 最好) |
| Extra | 额外信息 | ⭐ Using filesort/temporary 要警惕 |
| partitions | 分区表命中情况 | 一般忽略 |
type 字段速查表(核心)
性能从好到差 :system > const > eq_ref > ref > range > index > ALL。
- 至少要到
ref/range,出现ALL必须优化。 index比全表扫描好一点(扫索引树,但还是要全扫,常因覆盖索引)。
ini
EXPLAIN SELECT * FROM user WHERE id = 1;
-- type=const ✅ 主键等值
EXPLAIN SELECT * FROM user WHERE status = 1;
-- type=ALL ❌ status 无索引,全表扫描
key_len 判断联合索引用了几列
css
联合索引 (a, b, c),a 是 BIGINT(8字节)
key_len = 8 → 只用了 a
key_len = 16 → 用了 a, b
key_len = 24 → 用了 a, b, c
key_len 是判断联合索引用到第几列 的关键,常配合 ref、rows 一起看。
Extra 常见值
| Extra 值 | 含义 | 是否要优化 |
|---|---|---|
| Using index | 覆盖索引,不回表 | ✅ 最理想 |
| Using where | 服务层过滤 | 一般正常 |
| Using filesort | 无法用索引排序,额外排序 | ⚠️ 大数据量要优化 |
| Using temporary | 用了临时表 | ⚠️ 高成本,必须优化 |
| Using join buffer | JOIN 无索引用块嵌套 | ⚠️ 加 JOIN 索引 |
二、索引失效 8 种场景
| # | 场景 | 反例 | 原因 |
|---|---|---|---|
| 1 | 函数操作 | WHERE YEAR(create_time)=2026 |
破坏索引有序性 |
| 2 | 隐式类型转换 | phone=13800138000(phone 是 varchar) |
类型不匹配转全扫 |
| 3 | 左模糊 | name LIKE '%张' |
B+ 树无法定位 |
| 4 | OR 连接 | a=1 OR b=2(b 无索引) |
一半走全扫 |
| 5 | != / NOT IN | status != 1 |
优化器选全扫 |
| 6 | NULL 判断 | col IS NULL(索引未允许 NULL) |
视建索引而定 |
| 7 | 计算操作 | id+1 = 10 |
破坏了索引列 |
| 8 | 联合索引非最左 | 索引(a,b,c),WHERE b=1 |
违反最左前缀 |
三、SQL 重写优化 4 招
招 1:避免 SELECT *
sql
-- ❌ 全列查询,无法覆盖索引
SELECT * FROM user WHERE name = '张三';
-- ✅ 只查需要的列,可能命中覆盖索引
SELECT id, name, age FROM user WHERE name = '张三';
招 2:避免对索引列做函数/计算
sql
-- ❌ 函数包裹索引列
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
-- ✅ 改成范围查询
SELECT * FROM orders
WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01';
招 3:避免左模糊,用右模糊
sql
-- ❌ 左模糊,索引失效
WHERE name LIKE '%张';
-- ⚠️ 全模糊也失效,必要时用全文索引
WHERE name LIKE '%张三%';
-- ✅ 右模糊可走索引
WHERE name LIKE '张%';
招 4:OR 改成 UNION ALL
sql
-- ❌ a 有索引 b 无索引,整体全扫
SELECT * FROM user WHERE a = 1 OR b = 2;
-- ✅ 拆成两个查询
SELECT * FROM user WHERE a = 1
UNION ALL
SELECT * FROM user WHERE b = 2;
UNION ALL 比 UNION 快(不去重),生产配合
EXPLAIN确认是否值得。
四、慢查询排查流程
线上慢查询不是靠猜,有一套标准流程。
sql
-- 第 1 步:开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1s 记录
-- 第 2 步:查慢日志
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
-- 第 3 步:EXPLAIN
EXPLAIN SELECT * FROM user WHERE name = '张三';
-- 第 4 步:加索引
ALTER TABLE user ADD INDEX idx_name(name);
五、高频面试题
Q1: EXPLAIN 哪个字段最重要?
type 和 rows、Extra 三个最关键。type 看访问类型(ALL 全表扫描必须优化),rows 看预估扫描量,Extra 中的 Using filesort / Using temporary 是性能杀手。
Q2: type 字段从好到差怎么排?
system > const > eq_ref > ref > range > index > ALL。至少要到 ref/range,ALL 必须加索引。
Q3: 加了索引还是慢怎么办?
先 EXPLAIN 看 key 是否真用上索引,key_len 看联合索引用了几列。没用上说明命中了失效场景(函数、左模糊、非最左前缀)。用上了还慢,看 rows 是否太大,考虑重写 SQL、限量、分库分表。
Q4: key_len 有什么用?
判断联合索引用到了第几列。一个 BIGINT 8 字节,联合索引 (a,b,c) key_len=16 说明用了 a、b 两列。
Q5: 索引失效有哪些常见场景?
函数操作、隐式类型转换、左模糊、OR 连接无索引列、!= / NOT IN、计算操作、联合索引非最左前缀、NULL 判断(部分情况)。
Q6: Using filesort 一定有问题吗?
不一定。小数据量排序很快。但是大数据量排序用它就慢------加排序字段到索引里,让排序走索引即可避免。
六、面试速记 10 条
| # | 要点 |
|---|---|
| 1 | EXPLAIN 核心看 type / rows / Extra |
| 2 | type 排序:const > ref > range > index > ALL |
| 3 | ALL = 全表扫描,必须优化 |
| 4 | key_len 判断联合索引用几列 |
| 5 | Using filesort / temporary 要警惕 |
| 6 | 索引失效 8 种:函数、左模糊、类型转换、OR、非最左 |
| 7 | *避免 SELECT ,用覆盖索引 |
| 8 | YEAR() 改范围查询,左模糊改右模糊 |
| 9 | 慢查询流程:slow_log → EXPLAIN → 加索引/重写 |
| 10 | 优化完用 EXPLAIN 复核,别凭感觉 |
七、总结
一句话总结:
慢查询优化核心是看懂 EXPLAIN:
type判断访问类型(ALL/聚合索引要警惕),rows看扫描量,Extra找 filesort/temporary。先慢日志定位,再 EXPLAIN 分析,能加索引加索引(注意 8 种失效场景),能重写重写(避免 SELECT *、函数包裹、左模糊、OR)。优化完一定要再 EXPLAIN 复核。