MySQL 索引优化速查表:3 分钟定位慢查询根因

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:keypossible_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 的三个条件(必须同时满足):

  1. ORDER BY 的列在同一个索引中
  2. ORDER BY 的列顺序和索引列顺序一致(不能跳列)
  3. 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

总结

三个原则

  1. EXPLAIN 先看 type,ALL 和 index 是红线------type=ALL 意味着无论如何都要优化,不存在"还好"的情况
  2. Extra 里的 filesort 和 temporary 是两根刺------拔掉它们的办法都是加合适的索引
  3. key_len 小了就是联合索引掉了链子------范围查询、跳列、不满足最左前缀,三者必居其一

行动项

  • 收藏本文,下次遇到慢查询按三步诊断法排查
  • 在团队的 Wiki 或飞书文档里加上排查日志模板,每次慢查询都记录
  • 把本文的速查表截图/导出,日常上线的 SQL 审查时逐条过一遍 EXPLAIN

相关推荐
feelmylife591 小时前
企业级 LLM Mass Gateway — 多租户高并发网关,真正难点是 streaming 下的配额计数
人工智能·后端
云技纵横2 小时前
一个 CompletableFuture,把 CPU 打满了:默认线程池的线上坑
后端
free352 小时前
垃圾回收(GC)入门:从手动管理到自动回收的底层逻辑与演进
后端·架构
洛卡卡了2 小时前
Claude Code 项目级 Skill:团队协作场景下的使用场景
人工智能·后端·claude
用户762352425912 小时前
SpringAi 1.1更新说明
后端
程序员黎剑2 小时前
我把算卦变成了一段if-else
java·vue.js·spring boot·后端·ai编程
SimonKing2 小时前
Base62编码生成6位不重复短码的背后设计
java·后端·程序员
IT_陈寒2 小时前
JavaScript的隐式类型转换又双叒坑了我一次!
前端·人工智能·后端
她说彩礼65万3 小时前
Asp.net core 返回类型
数据库·后端·asp.net