当前位置:首页 > 文章列表 > 数据库 > MySQL > MySQL EXPLAIN ANALYZE 实战:慢查询别只盯着 possible_keys

MySQL EXPLAIN ANALYZE 实战:慢查询别只盯着 possible_keys

来源:MySQL 官方文档 2026-06-02 10:36:06 0浏览 收藏

慢查询排查里,我最怕看到一句话:“这条 SQL 有 possible_keys,应该走索引了吧?”如果你也这么想,线上迟早会被 MySQL 教做人。possible_keys 只是候选索引,不代表执行器真的少扫了行;EXPLAIN 只是估算,不代表真实执行就按这个成本走。

这篇不照搬官方文档。我用一个订单列表慢查询来讲 MySQL 8.x 里怎么用 EXPLAIN ANALYZE 把“估计会扫多少行”和“实际扫了多少行”对上,再决定是改 SQL、补复合索引,还是先更新统计信息。适用版本:MySQL 8.0.18 及以上可使用 EXPLAIN ANALYZE,MySQL 8.4 LTS 也适用。

MySQL EXPLAIN ANALYZE 慢查询诊断思维导图
思维导图:慢查询治理别只看有没有索引,要把估算、实测、排序、回表和上线复查串起来。

业务场景:订单列表突然慢了

假设有一张订单表 orders,业务后台经常按用户、状态和时间倒序查最近订单。刚上线时数据少,接口 30ms;半年后订单到了千万级,偶尔一查 2 秒,慢日志里最常出现的是下面这条 SQL。

SELECT id, user_id, status, amount, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

很多同学第一反应是:给 user_id 建索引。问题是,线上表里同一个用户可能有几十万历史订单,只靠 user_id 过滤完还要按 created_at 排序,再从里面找 status='PAID'。这个索引看着有用,实际可能还是扫得很累。

先用普通 EXPLAIN 看方向

普通 EXPLAIN 的价值是快速判断访问方式、候选索引、实际使用索引、估算行数、是否需要额外排序。它不是最终答案,但能告诉你从哪开始怀疑。

EXPLAIN
SELECT id, user_id, status, amount, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

如果你看到 key=idx_user_id,不要立刻高兴。继续看 rowsfilteredExtra。如果 rows 很大,Extra 里还有 Using filesort,说明 MySQL 虽然用了索引定位用户,但排序和过滤仍然可能拖慢整体。

EXPLAIN ANALYZE 看真实执行

EXPLAIN ANALYZE 会实际执行查询,并输出执行计划里每一步的真实耗时、实际行数和循环次数。它适合在测试环境、灰度库或可控场景里验证假设。生产上直接跑要谨慎,尤其是会扫大表、会触发复杂 join 的 SQL。

EXPLAIN ANALYZE
SELECT id, user_id, status, amount, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

我看 ANALYZE 输出时,重点盯三件事:第一,估算行数和实际行数差多少;第二,哪个节点耗时最高;第三,loops 是不是让一个看起来很小的步骤重复了很多次。慢查询很多时候不是单步很慢,而是“小慢步”循环太多。

MySQL EXPLAIN ANALYZE 慢查询治理流程图
流程图:慢日志定位只是入口,真正的治理要经过实测、改写、索引和上线复查。

估算行数和实际行数差很多,先别急着加索引

如果 EXPLAIN 估算只扫几百行,ANALYZE 实际扫了几十万行,可能是统计信息不准、数据分布倾斜,或者条件组合的选择性被优化器估错了。这个时候盲目加索引,可能只是在错误判断上继续堆东西。

我一般会先做两件事:确认表统计信息是否长期没更新;再按业务维度查一下数据分布,比如某些大客户、热门状态、历史订单是否极端集中。优化器面对“平均值”时很冷静,业务面对“超级用户”时会很狼狈。

ANALYZE TABLE orders;

SELECT status, COUNT(*)
FROM orders
WHERE user_id = 10086
GROUP BY status;

这个场景更适合复合索引

对这条订单查询来说,真正稳定的思路通常不是单列 user_id,而是按过滤和排序关系设计复合索引。比如:

CREATE INDEX idx_user_status_time
ON orders(user_id, status, created_at DESC);

这个索引的目标很明确:先定位某个用户,再过滤状态,最后按创建时间倒序拿前 20 条。这样 MySQL 不需要从大量用户订单里再额外排序,也更容易用索引顺序满足 ORDER BY created_at DESC LIMIT 20

别看到 Using filesort 就条件反射

Using filesort 很容易吓人,但它不是“用了磁盘排序”的同义词,也不是一定要立刻修。你要结合实际行数和耗时看。如果排序只有几十行,影响很小;如果排序前扫了几十万行,那才值得认真处理。

我会把 filesort 放回上下文里看:排序前有多少行?LIMIT 能不能提前生效?索引顺序能不能覆盖过滤和排序?如果这些问题没回答清楚,单纯把 Using filesort 消掉,可能只是把问题挪到别的地方。

MySQL EXPLAIN ANALYZE SQL 案例图
案例图:关键不只是“用了哪个索引”,而是 rows、loops、filesort 和改写后的行数变化。

覆盖索引不是越宽越好

有人看到查询里返回 amount,就想把它也塞进索引:(user_id, status, created_at, amount)。这确实可能减少回表,但也会让索引更大、写入更重、缓存命中更差。

我的习惯是先让复合索引解决过滤和排序,再看回表是否真的是瓶颈。慢查询优化不要一步到位堆一个超宽索引,尤其是订单表这类写入压力大的表。索引不是免费的,写入、更新、buffer pool、备份体积都会跟着付账。

SQL 改写:只取你真的需要的列

后台列表页经常一开始就 SELECT *,后来字段越加越多,回表成本也越来越高。排查慢查询时,先把页面真的需要的列列出来,能少取就少取。

SELECT id, amount, created_at
FROM orders
WHERE user_id = 10086
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

如果列表页只展示金额和时间,就别把收货地址、备注、扩展 JSON 一起拉回来。SQL 改写不是炫技,很多时候就是把需求边界收干净。

上线前后怎么复查

上线前,我会在预发或影子库里对比三组数据:旧 SQL 普通 EXPLAIN,旧 SQL EXPLAIN ANALYZE,新 SQL 加新索引后的 EXPLAIN ANALYZE。重点记录扫描行数、实际耗时、是否仍有大规模 filesort、返回行数是否一致。

上线后不要只看平均耗时,至少看 P95、P99、慢日志数量、Handler_read_next 这类读放大指标,以及写入延迟有没有被新索引拖慢。优化读查询时把写入打爆,是我见过很多团队踩过的坑。

我的慢查询 review 清单

  • 这条 SQL 的业务入口是什么?是否真的是当前最慢路径?
  • 普通 EXPLAINkeyrowsExtra 是否和预期一致?
  • EXPLAIN ANALYZE 里的实际行数和估算行数差多少?
  • 慢点在过滤、排序、回表、join,还是 loops 过多?
  • 复合索引是否同时服务过滤条件和排序?索引列顺序有没有按业务选择性设计?
  • 新索引是否会明显增加写入成本和存储成本?
  • 上线后是否有慢日志、P99、读放大和写入延迟复查计划?

最后说句实话

MySQL 慢查询治理最容易走偏的地方,是把 EXPLAIN 当成答案。它更像问诊单,不是手术方案。真正能救命的是你把估算和真实执行对上,把业务数据分布看明白,再决定改 SQL 还是改索引。

我的经验是:别迷信某一个字段,也别迷信某一个索引。每次优化都留下前后对比、SQL 样本、执行计划和上线复查结果。这样下一次慢查询来的时候,你不是从玄学开始,而是从证据开始。

参考资料:MySQL 8.4 Reference Manual:EXPLAIN StatementMySQL 8.4 Reference Manual:Using EXPLAIN

版本声明
本文转载于:MySQL 官方文档 如有侵犯,请联系study_golang@163.com删除
Go rand/v2 实战:抽奖、灰度和测试随机数别再混着用Go rand/v2 实战:抽奖、灰度和测试随机数别再混着用
上一篇
Go rand/v2 实战:抽奖、灰度和测试随机数别再混着用
MySQL InnoDB 死锁实战:别只会等 lock wait timeout
下一篇
MySQL InnoDB 死锁实战:别只会等 lock wait timeout
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    5904次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    6333次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    6142次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    8117次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    6632次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码