EXPLAIN说这一步扫87行,实际跑下来扫了41万行
本文目录
- rows这个数字,到底是从哪儿来的?
- 官方措辞已经把话说得很明白
- 估算是怎么算出来的
- 什么时候估算会特别不准
- 估算错了多少,怎么量出来?
- EXPLAIN ANALYZE把两个数字并排放
- 判据:差两个数量级就别看别的了
- 两个时间数字的读法
- loops这个数字才是嵌套循环的代价所在
- 用完记得关灯
- 哪几个字段能单独否决一条SQL?
- 能单独否决的三个
- 只能做程度判断的几个
- 出现了反而是好事的两个
- id列不是执行顺序
- 直方图在8.4版为什么终于变得可用?
- 直方图补的是索引统计的盲区
- 8.0时代直方图的真正问题
- 8.4把这件事自动化了
- ANALYZE TABLE会加读锁
- 同一条SQL,在两台机器上为什么给出不同的计划?
- 五个会改变计划的变量
- optimizer_switch是最容易被遗忘的那个
- 把计划本身存档,比事后回忆靠谱
- 优化器选错了索引,能强行掰过来吗?
- 三种干预手段,强度递增
- 索引提示的代价往往在半年后才显现
- 什么时候索引提示是合理的
- 独立站的慢查询,为什么总是同样那几类?
- 元数据表是重灾区
- 把数据搬出键值表才是根治
- 自动加载选项拖慢的是每一个请求
- 排序和分页的组合最贵
- 执行计划看完了,接下来改什么?
- 先修统计信息,成本最低
- 再看索引,注意顺序而不只是有没有
- 然后才是改SQL
- 最后考虑缓存与架构
- 这些慢查询最后是怎么变成SEO损失的?
- 第一环是首字节时间
- 第二环是抓取速率
- 第三环是慢查询日志本身
- 照着这套顺序把一条慢查询走完
- 常见问题解答
- EXPLAIN和EXPLAIN ANALYZE到底该用哪个?
- rows显示只有几十行,为什么查询还是很慢?
- Extra里出现Using filesort是不是一定要消除?
- 什么时候该建直方图,什么时候该建索引?
- MySQL 8.4升级之后,原来的直方图会自动更新吗?
- ANALYZE TABLE可以随时执行吗?
- 权威参考资料
摘要:EXPLAIN那一列rows是猜的,而且是从20个随机索引页上采样猜出来的。绝大多数“看了执行计划还是不知道为什么慢”的场景,症结都不在你没看懂哪个字段,而在于你把一个估算值当成了事实。真正能定位问题的动作是把估算和真实值放在一起对比——MySQL 8.0.18之后这件事有了现成工具。下面把rows从哪儿来、哪几个字段能单独否决一条SQL、直方图在8.4版为什么终于变得可用,以及独立站最典型的那几类慢查询长什么样,一次说透。
先做一个小实验。找一张几十万行的表,跑一次EXPLAIN,记下rows那一列的数字。然后跑一次ANALYZE TABLE,再EXPLAIN一次。
你大概率会看到一个不同的数字。表没动,SQL没改,索引没变,但MySQL给出的行数估算变了。
这不是bug。这是理解执行计划的起点:rows从来就不是一个测量结果,它是一个统计推断。而绝大多数人在读执行计划时,是把它当成事实在读的。
rows这个数字,到底是从哪儿来的?
官方措辞已经把话说得很明白
MySQL 8.4手册里EXPLAIN输出格式那一节对rows列的说明只有一句:对于InnoDB表,这个数字是一个估算值,未必总是精确的。
一句话,没有展开,也没有加粗。它就这么安静地待在一张字段说明表里,然后被几乎所有中文教程翻译成“扫描行数”,一个听起来像是测量结果的词。
估算是怎么算出来的
InnoDB维护着一套持久化的索引统计信息,包括每个索引的基数(不重复值的个数)。手册里配置持久化优化器统计参数那一节说明了这套统计的来源:它不是全表扫出来的,是采样出来的——默认从索引里随机挑20个叶子页,看看里面的值分布,然后按比例外推到整张表。
20个页面。一张千万行的表,一个索引可能有几万个叶子页,MySQL看了其中20个,然后告诉你这个查询会扫多少行。
采样页数是可调的,innodb_stats_persistent_sample_pages这个参数决定它。调大能提高准确度,代价是每次统计重算变慢,而且是全局生效。比起盲目调大这个全局参数,更值得做的是对确实有问题的那几张表单独处理——毕竟一个站点里真正会把优化器带偏的表,通常就那么两三张。
这不是MySQL偷懒。全量统计一张大表的代价高到不可接受,而查询优化必须在毫秒级完成。采样是这个约束下的唯一解,代价就是估算会错,有时错得很离谱。
什么时候估算会特别不准
三种情况最典型。第一种是数据分布严重倾斜:一列上99%的值是同一个,采样很可能全落在那99%里,于是MySQL认为这一列的区分度极低,索引不值得用。
第二种是数据刚刚发生大规模变动:批量导入、批量删除之后,统计信息还停留在变动前,而InnoDB的后台统计线程有自己的触发阈值,不一定立刻更新。
第三种是非索引列上的条件。索引列至少还有基数统计,非索引列在8.0之前压根没有任何分布信息,优化器只能拍一个固定的经验值。
估算错了多少,怎么量出来?
这是整篇文章里最实用的一节。
EXPLAIN ANALYZE把两个数字并排放
MySQL 8.0.18引入了EXPLAIN ANALYZE。MySQL官方博客介绍这条命令的那篇文章把它和普通EXPLAIN的根本差别讲得很清楚:它会真的把查询跑一遍,然后把优化器的估算和执行时的实测并排打印出来。
EXPLAIN ANALYZE
SELECT p.ID, p.post_title
FROM wp_posts p
JOIN wp_postmeta m ON m.post_id = p.ID
WHERE m.meta_key = '_stock_status'
AND m.meta_value = 'instock'
AND p.post_type = 'product'
ORDER BY p.post_date DESC
LIMIT 20;输出的每一行都带着一组括号,形如(cost=1234 rows=87) (actual time=0.03..412 rows=418203 loops=1)。前一组是估算,后一组是实测。
判据:差两个数量级就别看别的了
把这两组数字相除,就得到了一个非常好用的诊断指标。
| 估算与实测的比值 | 说明 | 该做什么 |
|---|---|---|
| 0.5倍到2倍之间 | 统计信息健康 | 继续看别的字段 |
| 相差5到20倍 | 有偏差但通常不致命 | 记下来,先看有没有更明显的问题 |
| 相差100倍以上 | 优化器是在盲选 | 先修统计信息,其他分析全部作废 |
第三行的重点是最后半句。当估算偏差达到两个数量级,优化器选的连接顺序、选的索引、选不选临时表,全部建立在一个错误的前提上。这时候去调整SQL写法、加索引、改join顺序,都是在错误的地图上找路。
两个时间数字的读法
actual time后面跟着两个用两点分隔的数字,第一个是拿到第一行所花的时间,第二个是拿完所有行的时间。这个区分比大多数人以为的有用。
如果第一个数字很小、第二个很大,说明这一步是流式产出的,慢在数据量上。如果两个数字都很大且接近,说明这一步是阻塞式的——它必须把所有数据处理完才能吐出第一行,典型的就是排序和分组。带LIMIT的查询如果第一个数字也很大,那基本可以断定LIMIT没有被下推,整个结果集被完整算了一遍才截前20条。
loops这个数字才是嵌套循环的代价所在
actual那一组括号里还有个容易被跳过的字段:loops。它表示这一步被执行了多少次。
在嵌套循环连接里,内层表的每一次查找都算一次loops。所以内层那一步就算单次只扫两行、耗时0.01毫秒,如果loops是二十万,总代价就是二十万次乘以0.01毫秒。
真正要看的是rows乘以loops,而不是rows本身。这也解释了一个常见的困惑:为什么执行计划上每一步的rows都不大,整条查询却慢得离谱。答案往往就藏在这个乘法里。
顺带说,这也是为什么连接顺序如此重要。把结果集小的表放外层,loops就小;放反了,同样的两张表、同样的索引,代价能差几个数量级。而连接顺序是优化器根据估算选的——又绕回到了那个估算准不准的问题上。
用完记得关灯
有一条安全提醒:EXPLAIN ANALYZE是真的会执行查询的。在生产库上对一条本来就跑几十秒的慢查询用它,你会实打实地再跑一次那几十秒。
比较稳妥的做法是先在从库上做,或者用EXPLAIN FOR CONNECTION去看正在跑的那条语句的计划,这个不会重新执行。找到那条语句的连接ID,然后:
EXPLAIN FOR CONNECTION 8123;这条命令在线上排查时特别顺手——你不需要复现,不需要构造参数,直接看那个正在把CPU烧起来的连接此刻在干什么。
哪几个字段能单独否决一条SQL?
执行计划一共十来个字段,但它们的分量完全不一样。有些字段看一眼就能下结论,有些只能做程度判断。这个区分能省掉大量无效分析。
能单独否决的三个
第一个是type。手册给出的连接类型从最好到最差依次是:system、const、eq_ref、ref、fulltext、ref_or_null、index_merge、unique_subquery、index_subquery、range、index、ALL。
其中最后两个是红线。ALL是全表扫描,index是全索引扫描——后者经常被误认为“用上索引了所以没问题”,实际上它扫的是整棵索引树,只是比扫数据页省点IO而已。一条面向用户的查询出现这两个值中的任何一个,基本就可以停止分析、直接去补索引了。
第二个是key为NULL。它意味着优化器考虑过所有可用索引,一个都没选。要么是真没有合适的,要么是统计信息骗了它。
第三个是Extra里出现Using temporary。手册的说明是:为了解析这个查询,MySQL需要创建一张临时表来保存结果,这通常发生在GROUP BY和ORDER BY列出的列不一致的时候。临时表如果超出内存限制就会落盘,落盘的临时表是数量级级别的性能悬崖。
只能做程度判断的几个
rows本身就在这一类——前面已经说清楚为什么。filtered也是,手册对它的定义是被表条件过滤掉之后剩余行数的估算百分比,最大值100表示没有过滤发生,并且明确给了公式:rows乘以filtered就是要和下一张表做连接的行数。
举个手册里的例子:rows是1000、filtered是50.00,那么参与下一步连接的就是500行。这个乘积才是连接代价的真正来源,而很多人只盯着rows看。
Using filesort也属于程度判断。它的字面意思容易吓人,但手册说的是MySQL需要额外一趟来确定如何按排序顺序取行——排序键如果能放进内存,这一趟其实很便宜。真正要紧的是排序的数据量,而不是这个词出现与否。
出现了反而是好事的两个
Using index表示覆盖索引生效,查询需要的所有列都在索引里,不用回表。Using index condition是索引条件下推,把过滤条件推到存储引擎层去做,减少了回表次数。看到这两个可以放心。
id列不是执行顺序
最后纠正一个流传很广的误读。手册对id列的定义是:这是该SELECT在查询中的序号。它不表示执行顺序。真正表示读表顺序的是行的排列——手册说的是,输出里按MySQL处理语句时读取表的顺序来列出这些表。
要看清楚真实的执行顺序,用树形格式。手册里EXPLAIN语句那一节列出了可用的输出格式与各自的适用条件:
EXPLAIN FORMAT=TREE SELECT ...;树形输出是自底向上执行的,缩进层级直接对应了执行的嵌套关系,比在表格里靠id猜要可靠得多。
直方图在8.4版为什么终于变得可用?
这一节讲的是估算不准这个问题的正面解法,而它在2024年之后发生了一次关键变化。
直方图补的是索引统计的盲区
MySQL 8.0引入了列直方图,它解决的是非索引列没有分布信息这个问题。你可以对任意一列建直方图,让优化器知道这列的值是怎么分布的。手册里优化器统计信息那一节划清了两者的分工:索引统计回答的是这列有多少个不同的值,直方图回答的是这些值各占多少比例——前者是一个数,后者是一条曲线。
这个分工差别在倾斜数据上体现得最明显。一列有三个不同值,索引统计只知道基数是3,于是优化器默认每个值各占三分之一;而真实分布可能是99比0.9比0.1。直方图存的正是后面这条信息。
手册里ANALYZE TABLE语句那一节给出的语法是这样的:
ANALYZE TABLE orders
UPDATE HISTOGRAM ON order_status, payment_method
WITH 64 BUCKETS;桶数的范围是1到1024,省略这个子句时默认是100个桶。桶越多越精细,但也越占空间、越慢。
8.0时代直方图的真正问题
不是精度,是维护。直方图建好之后不会自己更新——数据变了,直方图还是老的,而一个过期的直方图比没有直方图更危险,因为优化器会非常信任它。
于是8.0时代的直方图陷入了一个尴尬处境:它能解决问题,但它要求你记得定期重建,而“记得定期做某件事”正是所有运维流程里最不可靠的一环。
8.4把这件事自动化了
MySQL 8.4给ANALYZE TABLE加了一个AUTO UPDATE子句。手册的原话是:启用后,对这张表执行ANALYZE TABLE会自动更新直方图,使用该表上一次通过WITH ... BUCKETS指定的桶数;此外,在为该表重新计算持久化统计信息时,InnoDB的后台统计线程也会更新直方图。MANUAL UPDATE则禁用自动更新,且它是默认设置。
ANALYZE TABLE orders
UPDATE HISTOGRAM ON order_status
WITH 64 BUCKETS AUTO UPDATE;注意最后那句:MANUAL是默认。也就是说升级到8.4并不会让你已有的直方图自动获得这个能力,你得显式重建一次。这是个很容易漏掉的迁移动作。
把这条和前面那条放在一起看,8.0到8.4之间直方图最大的变化不在精度上,而在于它终于不再依赖有人记得去更新它。这类改动在数据库演进里往往比性能提升更值钱。
ANALYZE TABLE会加读锁
有个执行时机上的注意事项。手册写明:在分析期间,对InnoDB和MyISAM表会加读锁。
读锁意味着写入被阻塞。对一张大表跑ANALYZE TABLE,这个阻塞时间虽然通常很短,但在流量高峰期足以造成一波连接堆积。把它放进低峰期的定时任务里,别在排查问题的当下随手敲一条——那正是流量最需要被照顾的时候。这类维护动作的编排可以参考用cron把独立站服务器运维自动化那篇里的时间窗划分。
同一条SQL,在两台机器上为什么给出不同的计划?
这是排查过程中最容易让人怀疑人生的一幕:本地跑得好好的查询,上了生产就换了一套执行计划。
五个会改变计划的变量
| 变量 | 怎么影响 | 怎么核对 |
|---|---|---|
| 统计信息新鲜度 | 估算变了,选择就变 | 两边各跑一次ANALYZE TABLE后再比 |
| 数据量与分布 | 测试库数据少,全表扫更划算 | 比较两边的表行数量级 |
| MySQL版本 | 优化器策略逐版本在变 | 核对到小版本号 |
| optimizer_switch开关 | 某些优化被单独关掉过 | 两边各查一次该变量的值 |
| 缓冲池大小 | 影响优化器对IO代价的估计 | 比较innodb_buffer_pool_size |
前两个占了实际案例的绝大多数。拿一个数据量差两个数量级的测试库去验证生产的执行计划,本身就是一个没有意义的动作——在一万行的表上,全表扫描往往真的比走索引快,优化器选它是对的。
optimizer_switch是最容易被遗忘的那个
这个变量里装着十几个开关,控制着索引合并、条件下推、子查询物化等一系列优化能不能用。它经常在某次排查中被临时关掉一个,然后就再也没打开过。
SELECT @@optimizer_switch\G把两台机器的输出diff一下,几秒钟的事,但能省掉半天的猜谜。这类“某人某次改了但没记录”的配置漂移,是所有环境差异问题的共同根源。
把计划本身存档,比事后回忆靠谱
一个成本很低的习惯:在每次大版本上线之前,把核心查询的执行计划输出存一份到文件里,跟代码一起提交。等哪天出问题,你可以直接diff两份计划,而不是凭印象说“以前好像不是这样的”。
优化器选错了索引,能强行掰过来吗?
能,但这是一条要谨慎走的路。
三种干预手段,强度递增
最轻的是给优化器补足信息——ANALYZE TABLE、建直方图,让它自己选对。这是唯一一种不会在未来变成技术债的做法。
中等强度是优化器提示,写在SELECT后面的注释块里,比如/*+ JOIN_ORDER(a, b) */。它作用于某个具体的优化决策,语义相对精确。
最重的是索引提示,FORCE INDEX (idx_name)直接命令优化器用哪个索引。
索引提示的代价往往在半年后才显现
问题不在于它当下有没有效,而在于它把一个瞬时的判断永久固化进了代码。
写下FORCE INDEX的那一刻,你是对的——在当时的数据分布下,那个索引确实更好。半年之后数据量涨了十倍、分布变了、你还加了个更合适的新索引,而这条语句依然忠实地强制使用着那个已经不合适的旧索引,并且不会有任何报错。
更糟的是删索引时:被FORCE INDEX引用的索引一旦被删,语句直接报错,而不是优雅降级。这意味着索引清理这件事的风险,被这行提示悄悄放大了。
什么时候索引提示是合理的
有两种情况可以接受。一是紧急止血:线上已经在冒烟,先用提示把它按住,同时开一张单去修根因。二是优化器确实无解的边缘场景,比如数据分布极端到统计信息无论怎么更新都描述不了。
无论哪种,都应该在代码里留一行注释写清楚为什么加、什么条件下可以删。没有注释的FORCE INDEX,等于给后来的人留了一颗定时炸弹,而拆弹说明书在你脑子里。
独立站的慢查询,为什么总是同样那几类?
把前面的方法论落到具体场景上。做独立站的话,慢查询的形态其实相当集中。
元数据表是重灾区
WordPress和WooCommerce把大量业务字段存在wp_postmeta里,这是一张典型的键值表:post_id、meta_key、meta_value三列打天下。
问题出在meta_value上。它是longtext类型,没法建普通索引;就算建了前缀索引,区分度也往往很差——比如库存状态这个键,值只有两三种。于是任何“按某个自定义字段筛选商品”的查询,都注定要在这张表上扫大量行。
用EXPLAIN ANALYZE跑一次就能看到这个形态:估算说要扫几十行,实测扫了几十万行。一张商品数上万的店,postmeta的行数通常是商品数的三四十倍。
保哥前段时间看过一个做运动营养品的客户,商品一千二百多个,wp_postmeta是四十七万行。他们的商品筛选页在后台点一下要转七八秒,前台带筛选参数的分类页TTFB稳定在两秒以上。执行计划上type是ref、key也不是NULL,看起来一切正常——直到跑EXPLAIN ANALYZE,才看到某一步的估算是rows=87而实测是rows=418203,差了四千八百倍。
根因是meta_key这一列的分布极度倾斜:几十个不同的键里,有三个键占了七成以上的行。索引统计只知道这一列有几十个不同值,于是优化器认为按某个键筛选能过滤掉绝大部分行。建了直方图之后,这一步的估算从87变成了三十九万出头,优化器随即换了连接顺序,那条查询从七秒多降到八百毫秒。
值得记下来的是:这个问题没有加任何索引,也没有改一个字的SQL。改的只是优化器手里那份关于数据长什么样的描述。
把数据搬出键值表才是根治
直方图能救急,但键值表这个结构本身的代价是跑不掉的。WooCommerce官方给出的方向是把订单数据从postmeta搬进专用的订单表,也就是高性能订单存储。这类迁移的收益很实在,风险也真实存在,动手前值得看看WooCommerce高性能订单存储迁移的十二步流程与回滚演练那篇里的分步清单。
判据挺简单:如果你的慢查询里反复出现同一张键值表,那不是查询的问题,是数据模型的问题。执行计划能告诉你哪一步慢,告诉不了你这张表本来就不该长成这样。
自动加载选项拖慢的是每一个请求
另一类更隐蔽的是wp_options表里的自动加载项。每次请求都会执行一条把所有autoload为yes的选项一次性读出来的查询,而这张表会被插件不断塞东西,塞到几MB也没人管。
这条查询的执行计划看起来完全正常——它就是要读那么多行。问题不在计划上,在数据量上。这也是执行计划分析的一个边界:它能告诉你查询是怎么执行的,回答不了“这些数据本来就不该存在”。
排序和分页的组合最贵
按时间倒序取第几页,这是列表页最常见的形态。深分页时LIMIT 10000, 20意味着MySQL要先算出前10020行再扔掉前10000行。执行计划上表现为rows很大而结果只有20条。
解法是把偏移量分页换成游标分页——用上一页最后一条的时间戳作为条件,而不是用偏移量。这个改动需要改前端逻辑,但它是唯一能让深分页代价保持恒定的做法。
执行计划看完了,接下来改什么?
诊断只是一半,另一半是知道该动哪里。按性价比排序如下。
先修统计信息,成本最低
如果估算与实测偏差超过两个数量级,第一动作永远是ANALYZE TABLE。这个动作零风险、秒级完成、不改任何代码,而且相当一部分“突然变慢”的案例到这一步就结束了。
值得建立的习惯是:批量导入、批量更新、大规模删除之后,主动跑一次ANALYZE TABLE,别等后台线程自己发现。
再看索引,注意顺序而不只是有没有
复合索引的列顺序决定了它能被用到什么程度。经验规则是等值条件列在前、范围条件列在后、排序列跟在等值列后面。
一个具体例子:WHERE status = 1 AND created_at > ? ORDER BY created_at DESC,正确的索引是(status, created_at)。反过来建成(created_at, status),status这一列就用不上了,因为范围条件之后的列无法继续用于索引查找。
然后才是改SQL
把SELECT星号换成具体列,这是覆盖索引能否生效的前提。把子查询改成JOIN,把OR拆成UNION,把函数从索引列上挪开——在索引列外面套一层DATE()会让索引直接失效,改成对时间的范围条件就好了。
最后考虑缓存与架构
能缓存的查询就别反复执行。对象缓存这一层的收益在读多写少的站点上非常可观,具体做法可以看Redis对象缓存给WordPress提速的原理与运维。再往外一层是全页缓存,Nginx fastcgi_cache全页缓存的配置与清理那篇讲的是让请求压根不到PHP和数据库。
要提醒的是顺序:先把查询本身修对,再加缓存。反过来做的话,缓存会把问题盖住,直到某次缓存失效时以更糟糕的形式爆出来——缓存击穿时涌向数据库的,正是那些你从没修过的慢查询。
这些慢查询最后是怎么变成SEO损失的?
数据库和搜索排名之间隔着好几层,但这条链路是通的。
第一环是首字节时间
动态页面的TTFB里,数据库时间往往占大头。一条慢查询让TTFB从200毫秒变成1.2秒,这个增量会直接进入最大内容绘制的计算,因为LCP必然发生在首字节之后。
多层缓存和TTFB的关系,TTFB与多层缓存对Core Web Vitals和抓取预算的双重影响那篇拆得比较细,这里只强调一点:数据库是这条链上最靠里的一环,它的抖动会被后面每一层放大。
第二环是抓取速率
搜索引擎会根据站点的响应表现调整抓取频率。响应持续变慢,抓取速率就会被下调,而且这个调整是保守的、恢复缓慢的。对商品页频繁变动的电商站,抓取速率下降意味着价格和库存的更新迟迟进不了索引。
这类由基础设施引发的SEO损失有个共同特征:它们在业务侧完全没有对应的动作,所以复盘时几乎不会有人往那个方向想。排名掉了,团队第一反应是查内容、查外链、查算法更新,而真凶是三个月前某次数据导入之后没人跑过的统计信息。同一类归因困难在证书层面也出现过,证书有效期砍向47天与续期流程的自动化改造那篇讲的就是另一种在服务器日志里留不下痕迹的故障。
第三环是慢查询日志本身
值得一提的是,慢查询日志开着但没人看,是个比想象中普遍的状态。日志文件涨到几百MB,里面真正需要处理的可能只有几十条——怎么从噪音里把信号捞出来,是另一个话题,也是本文这套执行计划分析真正开始起作用的地方:日志告诉你哪条SQL慢,执行计划告诉你它为什么慢。
顺带一提,服务器本身的负载排查是这条链的前置动作。如果机器已经在扛不住的边缘,任何SQL优化的效果都会被噪音淹没,先照着Linux服务器负载飙高时的CPU、内存与磁盘IO排查把基线确认了再说。
照着这套顺序把一条慢查询走完
最后把全文压成一条可执行的路径。
| 步骤 | 动作 | 看什么 | 下一步的分叉 |
|---|---|---|---|
| 1 | 拿到具体SQL | 慢查询日志或EXPLAIN FOR CONNECTION | 拿不到就先把日志配对 |
| 2 | EXPLAIN ANALYZE | 估算rows与实测rows的比值 | 差100倍以上跳第3步,否则跳第4步 |
| 3 | ANALYZE TABLE | 重跑一次比值是否收敛 | 收敛就结束,不收敛考虑直方图 |
| 4 | 看type与key | 是否为ALL或index、key是否为NULL | 命中就去补索引 |
| 5 | 看Extra | 有没有Using temporary | 有就查GROUP BY与ORDER BY是否一致 |
| 6 | 看actual time两个数字 | 第一个数字是否也很大 | 是则查LIMIT有没有被下推 |
| 7 | 改完再跑一次 | 比值与总耗时同时改善 | 只改善一个说明动错了地方 |
第7步的判据值得单独记:如果耗时降了但估算偏差没降,说明你是靠碰运气改对的,同一类问题还会在别的查询上复现。真正的修复应该让这两个指标一起变好。
还有一条元级别的建议。执行计划分析很容易变成一件“出了事才做”的救火动作,而它更适合作为例行体检——在每次大版本上线、每次数据迁移之后,把站点最核心的那五到十条查询的执行计划存档一份。等某天变慢了,你手上就有对照组,而不是只能对着当前的输出干瞪眼。这跟服务器配置需要基线是同一个道理,Docker发布端口后ufw规则形同虚设那篇里那份上线自查清单,本质上也是在做同一件事:把“应该是什么样”写下来,否则你永远只能看到“现在是什么样”。
常见问题解答
EXPLAIN和EXPLAIN ANALYZE到底该用哪个?
先用EXPLAIN看结构,判断有没有全表扫描、有没有临时表这类一眼能定性的问题。如果结构看起来没毛病但查询就是慢,再用EXPLAIN ANALYZE去对估算和实测。注意后者会真的执行查询,别在生产库上对一条跑几十秒的语句直接用,优先在从库上做。
rows显示只有几十行,为什么查询还是很慢?
最常见的原因就是这个估算不准。跑一次EXPLAIN ANALYZE看实测行数,如果实测是几十万而估算是几十,那就是统计信息过期或数据分布倾斜,先跑ANALYZE TABLE。另一个可能是rows只统计了单表的扫描行数,而实际代价来自嵌套循环的loops次数,这个也要看EXPLAIN ANALYZE才能发现。
Extra里出现Using filesort是不是一定要消除?
不一定。手册对它的定义是MySQL需要额外一趟来确定按排序顺序取行的方式,如果排序的数据量很小、排序键能放进内存,这一趟的开销可以忽略。真正需要警惕的是数据量大到排序要落盘的情况。判断方式是看排序前的行数,而不是看这个词出现与否。
什么时候该建直方图,什么时候该建索引?
索引解决的是怎么快速找到行,直方图解决的是优化器怎么估算有多少行。规则大致是:出现在WHERE里且需要快速定位的列建索引;出现在WHERE里但区分度低、不值得建索引、却又影响优化器判断的列建直方图。典型的直方图适用场景是状态类、类型类的枚举列,它们的值分布往往极不均匀。
MySQL 8.4升级之后,原来的直方图会自动更新吗?
不会。手册明确写了MANUAL UPDATE是默认设置,所以升级本身不会改变已有直方图的行为。要用上自动更新,需要显式重新执行一次带AUTO UPDATE子句的ANALYZE TABLE。这是升级时很容易漏掉的一步。
ANALYZE TABLE可以随时执行吗?
技术上可以,但手册写明分析期间会对表加读锁,写入会被阻塞。大表上这个阻塞虽然通常很短,也足以在流量高峰造成连接堆积。建议放进低峰期的定时任务,并在批量导入或大规模删除之后主动触发一次,而不是等后台统计线程自己发现数据变了。
权威参考资料
本文标题:《EXPLAIN说这一步扫87行,实际跑下来扫了41万行》
本文链接:https://zhangwenbao.com/mysql-explain-estimate-vs-actual-rows-diagnosis.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0