MySQL 索引优化速查表:3 分钟定位慢查询根因
适用:MySQL 8.0+ | 速查向 | 配合本号 MySQL 系列文章使用效果更佳
引言
线上报警:订单查询又超时了。
打开慢查询日志,面前是一条复杂的 SQL------三表 JOIN、两个子查询、WHERE 里混着 OR 和 LIKE。你深吸一口气,准备开始排查。
第一眼看什么?
大部分人会直接看 SQL 逻辑------"是不是 JOIN 太多了?是不是子查询太深了?"这些当然要想,但更快的路径是:先跑一遍 EXPLAIN,3 个字段就能定位 80% 的问题。
这篇文章就是一张"慢查询急诊室"的速查表。不需要从头学索引原理,只要你拿到 EXPLAIN 结果,对着这张表查,3 分钟找到根因。
💡 本文是速查向。如果你想从头理解每个场景的原理,推荐先读本号的 MySQL 三部曲:《索引失效的 10 个场景》《NULL 值的唯一索引陷阱》《慢查询优化实战》。在公众号后台分别回复「索引失效」获取。
诊断三步法:拿到 EXPLAIN,按这个顺序看
sql
拿到 EXPLAIN 结果
│
▼
┌─────────────┐
│ 第一步:type │──→ ALL / index?──→ 索引没走或全扫描 → 速查表 1
└─────────────┘
│ type 正常(ref / range / const)
▼
┌─────────────┐
│ 第二步:key │──→ 走的索引不对?──→ 速查表 2
│ + key_len │──→ key_len 太小?──→ 联合索引只用了一部分 → 速查表 3
└─────────────┘
│ key 正确、key_len 合理
▼
┌─────────────┐
│ 第三步:Extra│──→ Using filesort?──→ 排序没走索引 → 速查表 4
│ + rows │──→ Using temporary?──→ 分组/去重没走索引
│ │──→ Using where?──→ 回表后过滤,考虑覆盖索引
└─────────────┘
下面逐个展开。每个诊断结果都配有"直接能用的修复方案",不废话。
速查表 1:type 字段 --- 一票否决
type 是 EXPLAIN 里最重要的字段。它告诉你 MySQL 怎么访问数据------从好到差有严格的排序:
sql
const > eq_ref > ref > range > index > ALL
🟢 🟢 🟢 🟡 🟠 🔴
| type | 含义 | 常见场景 | 要不要优化? |
|---|---|---|---|
| const | 主键或唯一索引等值查询,最多 1 行 | WHERE id = 1 |
🟢 最优,无需优化 |
| eq_ref | JOIN 时驱动表的每一行在被驱动表中只匹配一行 | JOIN ON a.id = b.user_id(b.user_id 有唯一索引) |
🟢 很好 |
| ref | 非唯一索引等值查询,可能多行 | WHERE user_id = 8888 |
🟢 正常,继续看 Extra |
| range | 索引范围扫描 | WHERE create_time > '2025-01-01' |
🟡 范围查询的正常表现。关注 rows 数量 |
| index | 扫描整个索引(不是全表,但不过滤) | SELECT COUNT(*) / ORDER BY col 没有 WHERE |
🟠 通常需要优化 |
| ALL | 全表扫描 | 没走索引、索引失效、优化器放弃索引 | 🔴 必须优化! |
🔴 type=ALL 的 6 种常见根因 → 修复
| 根因 | EXPLAIN 特征 | 修复 |
|---|---|---|
| 没有索引 | possible_keys = NULL, key = NULL |
CREATE INDEX 建索引 |
| LIKE 前置通配 | WHERE col LIKE '%xxx' |
改成 LIKE 'xxx%',或用全文索引 |
| 索引列套了函数 | WHERE YEAR(col) = 2025 |
改成范围查询 col >= '2025-01-01' AND col < '2026-01-01' |
| 隐式类型转换 | VARCHAR 列比较数字 | 给值加引号 WHERE phone = '13800138000' |
| OR 中一侧无索引 | WHERE idx_col = 1 OR no_idx_col = 2 |
拆成 UNION ALL |
| 优化器主动放弃 | possible_keys 有值但 key = NULL |
rows 占比 > 20-30%,考虑覆盖索引或 LIMIT |
💡 排查优先级 :如果你看到
possible_keys有值但key是 NULL → 说明索引存在但没生效 → 优先排查"索引列套函数"和"隐式类型转换",这两个最常见也最隐蔽。
速查表 2:key 和 possible_keys --- 索引走对了吗?
如果 type 不是 ALL,接着看 key------实际用的索引是不是你期望的那个?
| 情况 | possible_keys | key | 含义 | 动作 |
|---|---|---|---|---|
| ✅ 一切正常 | 有你建的索引 | 就是它 | MySQL 按你的预期走了索引 | 继续看 Extra |
| ⚠️ 走了错索引 | 有更好的索引 | 用了别的 | MySQL 选错索引(统计信息不准) | ANALYZE TABLE 更新统计信息,或 FORCE INDEX |
| ⚠️ 没走联合索引 | 联合索引在列表中 | 走了单列索引 | 联合索引不满足最左前缀 | 检查 WHERE 条件是否包含联合索引的首列 |
| 🔴 索引没生效 | NULL | NULL | 索引存在但用不上 | 回到速查表 1 排查根因 |
快速验证:你的 SQL 用上了哪个索引?
sql
-- 不用 EXPLAIN 也能快速看索引使用情况
EXPLAIN SELECT * FROM orders WHERE user_id = 8888 ORDER BY create_time DESC LIMIT 20;
-- 关注这三个字段:
-- possible_keys: MySQL 考虑了哪些索引
-- key: 最终选了哪个
-- key_len: 用了索引的多少字节(联合索引利用率的关键指标)
速查表 3:key_len --- 联合索引用了多少?
key_len 是排查"联合索引只用了一半"的核心指标。
先记住各类型的字节数:
| 列类型 | 字节数 |
|---|---|
| BIGINT | 8 |
| INT | 4 |
| TINYINT | 1 |
| DATETIME | 5(8.0 之前)+ 1(nullable 标记) |
| VARCHAR(N) | N × 4(utf8mb4)+ 2(长度前缀)+ 1(nullable) |
| CHAR(N) | N × 4(utf8mb4)+ 1(nullable) |
| 允许 NULL | 额外 +1 字节 |
⚠️
key_len的计算细节和字符集有关,上表以 utf8mb4 为例。实际排查时不需要精确计算------看 key_len 是否明显小于预期就行。
key_len 诊断
| 现象 | 含义 | 修复 |
|---|---|---|
| key_len = 首列字节数 | 联合索引只用了第一列 | WHERE 条件里第二列没起作用 → 检查是否跳过了中间列、或中间列是范围查询 |
| key_len < 预期 | 联合索引部分列生效 | 定位断档位置:范围查询后面的列全部失效 |
| key_len 比预期小 1 | 某列允许 NULL,多占了一字节 | 正常现象,无需处理 |
实际案例
联合索引 (user_id BIGINT, status TINYINT, create_time DATETIME),预期 key_len = 8 + 1 + 5 + 1 = 15(status 和 create_time 允许 NULL 时各 +1)。
sql
-- WHERE user_id = 100 AND status = 1 AND create_time > '2025-01-01'
-- key_len = 9 (= 8 + 1)
-- → 只有 user_id + status 参与查找,create_time 因为范围查询被"断档"
-- → 符合预期,范围查询后的列本来就用不上
💡 联合索引列顺序的黄金法则:等值查询列在前 → ORDER BY 列在中 → 范围查询列在最后。
速查表 4:Extra 字段 --- 是否"多做了事"
Extra 告诉你 MySQL 有没有做额外的工作。有些额外工作是正常的,有些是性能杀手。
| Extra 信息 | 严重程度 | 含义 | 修复 |
|---|---|---|---|
| Using index | 🟢 最优 | 覆盖索引,不回表 | 无需优化------查询所需的所有列都在索引里 |
| Using index condition | 🟡 正常 | 索引下推(ICP),先过滤再回表 | 大多数情况正常。如果 rows 很大,考虑覆盖索引 |
| Using where | 🟡 注意 | 回表后才过滤 | 检查 WHERE 条件能否加入索引。如果 rows 大,考虑覆盖索引 |
| Using filesort | 🔴 需要优化 | 额外排序(没走索引顺序) | 把 ORDER BY 列加入联合索引,且排在 WHERE 等值列之后 |
| Using temporary | 🔴 需要优化 | 用了临时表(GROUP BY / DISTINCT 无法利用索引) | 给 GROUP BY / DISTINCT 列建索引 |
| Using join buffer | 🟠 关注 | JOIN 时驱动表没走索引 | 给 JOIN 关联列加索引 |
| Backward index scan | 🟢 正常 | MySQL 8.0 反向扫描索引 | 无需优化------这是 MySQL 8.0 的新特性,比 filesort 快 |
filesort 速查:怎么改索引消除 filesort?
sql
-- ❌ 排序字段不在索引中
SELECT * FROM orders WHERE user_id = 8888 ORDER BY create_time DESC;
-- Extra: Using filesort
-- ✅ 把 ORDER BY 列加入索引
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time);
-- Extra: Backward index scan(filesort 消失了!)
消除 filesort 的三个条件(必须同时满足):
- ORDER BY 的列在同一个索引中
- ORDER BY 的列顺序和索引列顺序一致(不能跳列)
- ORDER BY 的排序方向统一(全 ASC 或全 DESC,MySQL 8.0 支持反向扫描)
速查表 5:rows --- 扫描行数合理吗?
rows 是优化器估算的扫描行数。用它和实际数据量做对比:
| rows vs 总行数 | 信号 | 动作 |
|---|---|---|
| rows < 100 | 🟢 精准定位 | 很好 |
| rows = 1%~10% 总行数 | 🟡 合理范围 | 关注 Extra 有无 filesort |
| rows = 10%~30% 总行数 | 🟠 偏高 | 考虑覆盖索引或加 LIMIT |
| rows > 30% 总行数 | 🔴 很可能全表扫描 | 优化器可能主动放弃索引,用 FORCE INDEX 验证 |
⚠️
rows只是估算值,可能不准。如果 EXPLAIN 的 rows 看起来太少或太多,可以用ANALYZE TABLE更新统计信息后再跑一次。
综合速查:按症状对号入座
不想一步步查?直接看症状找解法:
| 症状 | 最可能的 EXPLAIN 特征 | 优先排查 |
|---|---|---|
| "这条 SQL 跑了 5 秒" | type=ALL | ① 有没有索引 ② LIKE '%x' ③ 列套函数 |
| "明明有索引,怎么还全表扫描" | possible_keys 有值, key=NULL | ① 隐式类型转换 ② 列上套了函数 ③ 大范围扫描优化器放弃 |
| "分页越翻越慢" | rows 小但 LIMIT OFFSET 大 | 改成游标分页(WHERE id > last_id LIMIT 20) |
| "排序特别慢" | Extra: Using filesort | ORDER BY 列不在索引中,或排序方向不统一 |
| "GROUP BY 特别慢" | Extra: Using temporary | GROUP BY 列没建索引,建联合索引 |
| "JOIN 查询慢" | Extra: Using join buffer | 被驱动表的 JOIN 关联列没建索引 |
| "COUNT(*) 都慢" | type=index | 正常(COUNT 必须扫索引),考虑用 Redis 缓存计数 |
| "索引列数都对,但只用了一半" | key_len 偏小 | 联合索引断档:范围查询后面的列失效 |
附:排查日志模板
建议团队在慢查询排查时统一用这个格式记录,方便后续复盘:
markdown
【慢查询排查记录】
原始 SQL: _______________________________________________
执行时间: _______ms 行数: _______
EXPLAIN 结果:
type: _______
key: _______ (possible_keys: _______)
key_len: _______
rows: _______
Extra: _______
根因定位: _______________________________________________
修复方案: _______________________________________________
修复后 type: _______ key_len: _______ 执行时间: _______ms
总结
三个原则
- EXPLAIN 先看 type,ALL 和 index 是红线------type=ALL 意味着无论如何都要优化,不存在"还好"的情况
- Extra 里的 filesort 和 temporary 是两根刺------拔掉它们的办法都是加合适的索引
- key_len 小了就是联合索引掉了链子------范围查询、跳列、不满足最左前缀,三者必居其一
行动项
- 收藏本文,下次遇到慢查询按三步诊断法排查
- 在团队的 Wiki 或飞书文档里加上排查日志模板,每次慢查询都记录
- 把本文的速查表截图/导出,日常上线的 SQL 审查时逐条过一遍 EXPLAIN