MySQL出现慢查询时,很多开发人员第一反应是给SQL加索引,但真正专业的数据库优化通常应该从执行计划开始。EXPLAIN能够告诉我们MySQL准备怎样访问表、可能使用哪些索引、实际选择了哪个索引,以及优化器预计需要读取多少数据。
但EXPLAIN最容易被误解的地方也在这里。type=ALL并不意味着每一次查询都一定需要优化,type=range也不代表查询已经足够优秀,Using filesort更不是看到就必须创建索引。执行计划必须结合SQL本身、数据分布、索引结构、返回行数和实际执行时间一起分析。
对于SaaS系统尤其如此。一条查询即使只有几十毫秒,如果每秒执行几千次,最终消耗的数据库资源可能远远超过一条偶尔执行几秒的后台报表SQL。因此,EXPLAIN应该成为性能分析工具,而不是一张简单的“性能排行榜”。
最基本的用法是:
EXPLAIN
SELECT *
FROM users
WHERE id = 1;
如果使用的是支持更详细执行分析的MySQL版本,还可以使用:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE id = 1;
两者最大的区别在于,普通EXPLAIN主要展示优化器生成的执行计划,而EXPLAIN ANALYZE会实际执行语句,并提供实际执行过程中获得的运行信息。因此,对于已经存在的慢查询,EXPLAIN ANALYZE往往比单纯猜测更加有价值。不过对UPDATE、DELETE等有副作用的语句不能直接为了分析而随意执行,生产环境尤其需要谨慎。
理解EXPLAIN,首先应该看id、select_type和table。id用于标识SELECT层级,在包含子查询、UNION等复杂SQL时可以帮助理解执行计划结构。它并不是一个“越小越好”的性能指标。简单SQL通常只有一个SELECT层级,因此看到id=1并没有什么特殊意义。
select_type则描述SELECT属于哪一种查询结构。常见的有SIMPLE、PRIMARY、SUBQUERY、UNION等。看到SUBQUERY或者DEPENDENT SUBQUERY也不能直接宣布“查询性能有问题”。关键还是要看这个子查询实际执行多少次、扫描多少数据,以及能否通过改写SQL降低重复计算。
table表示当前执行计划步骤涉及的表。对于多表JOIN,应该结合各表出现的顺序、连接条件和索引情况一起观察。这里开始进入真正有价值的性能分析,因为JOIN顺序和访问路径可能直接决定数据库需要读取多少数据。
然后是很多开发人员最喜欢盯着看的type。
常见的访问类型包括system、const、eq_ref、ref、range、index和ALL。通常来说,在其他条件相近的情况下,const、eq_ref等精准访问方式的扫描范围比较小,而ALL通常意味着全表扫描。但是不能把它们理解成一个绝对的“性能从高到低排行榜”。
例如一个只有几十行的小表,即使EXPLAIN显示type=ALL,全表扫描也可能是最合理的方案。为了读取几十行数据而强行建立索引,反而增加索引维护成本。另一方面,一个大型表即使显示type=range,如果范围覆盖了数百万行,也可能依然非常昂贵。
因此,真正应该关注的是“这个访问类型导致MySQL实际需要读取多少数据”。
例如:
SELECT *
FROM orders
WHERE tenant_id = 1001
AND status = 'pending';
如果orders表有数千万行,而执行计划显示需要读取非常大的数据范围,就需要检查是否存在符合真实查询模式的复合索引。例如查询经常按照tenant_id和status过滤,那么可能需要研究类似:
CREATE INDEX idx_orders_tenant_status
ON orders (tenant_id, status);
但到底应该把created_at放在索引的什么位置,还需要结合WHERE、ORDER BY、数据选择性和实际查询模式决定。不存在一条适用于所有SaaS系统的固定索引顺序。
possible_keys表示优化器认为可能用于当前查询的索引,而key表示最终选择使用的索引。如果possible_keys存在而key为NULL,并不一定意味着MySQL出错。优化器可能判断全表扫描成本更低,或者现有索引无法有效降低需要读取的数据量。
如果key为NULL,则应该进一步问三个问题:查询条件是否真的有合适的索引,现有索引的选择性是否足够,以及优化器估算的成本是否认为使用索引反而更贵。
key_len表示执行计划实际使用的索引部分长度,单位通常是字节。它不能简单理解为“VARCHAR(255)就是255字节”。实际长度会受到字符集、数据类型、NULL属性以及索引中实际使用了多少列等因素影响。因此,看到key_len时应该结合具体字段定义分析,而不是直接按照字符数量换算。
ref用于显示索引比较所使用的列或常量。例如一个JOIN可能显示某个表的列作为匹配条件。对于理解JOIN为什么能够通过索引快速定位数据,这个字段很有价值。
接下来是最值得关注的rows。
EXPLAIN中的rows通常是优化器估算需要检查的行数,而不是实际已经读取的精确行数。对于大型表,rows从几千变成几百万,通常值得认真调查。但仍然不能仅凭一个数字宣布SQL一定慢。
例如一条查询预计读取100万行,但最终只返回几十行,说明过滤效率可能很差;另一条查询预计读取100万行,却是后台每天运行一次的统计任务,影响可能完全不同。因此rows必须结合执行频率、实际延迟和业务场景判断。
如果使用EXPLAIN ANALYZE,还可以观察估算值与实际执行情况之间的差异。如果优化器认为只需要读取几百行,但实际读取了几十万行,就应该进一步调查统计信息、数据分布和执行计划是否出现偏差。
filtered则表示优化器估计经过当前条件过滤后剩余的比例。它需要和rows一起理解,而不是单独看百分比。比如预计扫描100万行,filtered只有1%,意味着最终可能只有约1万行通过该阶段的过滤,但数据库仍然可能已经付出了扫描大量数据的成本。
Extra经常是EXPLAIN中最容易被误读的部分。
Using where本身不是坏消息,它通常只是说明MySQL还需要利用WHERE条件过滤访问到的数据。
Using index表示查询可以通过索引完成所需的数据访问,也就是通常所说的覆盖索引场景。它往往可以减少回表读取,但并不意味着任何出现Using index的查询都是最快的。索引本身如果非常庞大,或者需要扫描大量索引记录,仍然可能产生明显成本。
Using filesort也不是“使用了磁盘文件排序”的简单意思。它表示MySQL需要执行额外的排序操作。现代MySQL可能在内存中完成,也可能根据数据量和内存条件使用临时文件。因此,不能看到Using filesort就立即创建一个ORDER BY索引。
真正应该问的是:排序了多少行?查询频率是多少?排序是否可以通过合适的索引减少?建立这个索引以后,是否又会明显增加写入成本?
例如:
SELECT id, title, created_at
FROM news
WHERE tenant_id = 1001
ORDER BY created_at DESC
LIMIT 50;
如果这是SaaS系统中非常高频的查询,那么围绕真实过滤条件和排序条件设计复合索引,就可能比单独给created_at建立索引更加合理。
Using temporary同样不是看到就意味着SQL一定有问题。GROUP BY、DISTINCT、ORDER BY以及复杂聚合等操作在某些执行计划中确实可能需要临时结构。真正需要关注的是临时数据规模、执行频率以及是否能够通过改变查询或索引减少中间数据。
例如一个每天运行一次、处理几百万行数据的报表查询,即使出现Using temporary,也不一定值得为了消除这个字段而牺牲在线交易性能。反过来,如果一个每秒执行数百次的API查询不断产生大量临时数据,那就是完全不同的问题。
因此,EXPLAIN优化不能变成“消灭所有Extra提示”的游戏。
一个非常典型的错误案例是:
SELECT *
FROM users
WHERE email LIKE '%@example.com';
假设email建立了普通B-Tree索引,这种左侧带通配符的LIKE通常无法利用该索引进行常规的前缀范围查找,因此可能需要检查大量数据。
但不能把它改成:
WHERE email LIKE '@example.com'
然后宣布性能优化成功,因为这已经改变了原始查询的业务含义。原查询寻找的是以@example.com结尾的邮箱,而修改后的查询寻找的是以@example.com开头的字符串,结果完全不同。
如果业务真正需要按照邮箱域名查询,更合理的做法可能是增加独立的email_domain字段,在写入用户资料时保存域名,并针对该字段建立索引。例如:
SELECT *
FROM users
WHERE email_domain = 'example.com';
然后:
CREATE INDEX idx_email_domain
ON users (email_domain);
这才是真正从数据模型层面解决问题,而不是为了让EXPLAIN出现range就修改查询语义。
分页也是类似的问题。LIMIT 1000能够限制返回结果数量,但不代表数据库一定只扫描1000行。如果使用很大的OFFSET:
SELECT id, title
FROM news
ORDER BY created_at DESC
LIMIT 1000 OFFSET 900000;
数据库仍然可能需要处理大量前面的结果才能找到目标页面。
对于不断向后翻页的新闻列表、订单列表或者日志系统,可以考虑Keyset Pagination,也就是利用上一页最后一条记录作为下一页查询起点。例如:
SELECT id, title, created_at
FROM news
WHERE created_at < '2026-09-20 10:00:00'
ORDER BY created_at DESC
LIMIT 50;
实际生产环境通常还需要使用稳定的复合排序键,例如created_at加id,以避免时间戳相同时出现分页重复或遗漏。
索引设计也不能只看单条SQL。一个索引可以让SELECT明显变快,却同时增加INSERT、UPDATE和DELETE成本。对于高写入SaaS表,几十个看似“有用”的索引可能最终把数据库拖慢。因此应该结合慢查询、高频查询、索引大小、写入成本以及真实访问比例进行综合判断。
尤其不要因为某条SQL没有使用索引,就机械地创建一个单列索引。如果查询条件是tenant_id、status、created_at三个字段,那么分别建立三个单列索引未必比一个符合真实访问模式的复合索引更好。
数据库优化还必须注意数据分布。一个status字段只有pending、paid、cancelled几种值,如果整个orders表中有几千万行,而pending占据其中很大比例,那么单独建立status索引未必能够带来理想效果。优化器最终是否使用这个索引,也会受到选择性和成本估算影响。
这也是为什么“WHERE字段都应该建索引”是一个危险的经验法则。
对于JOIN,则需要重点检查连接两侧是否存在合理索引,以及JOIN实际产生了多少中间数据。例如:
SELECT o.id, u.email
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.tenant_id = 1001
AND o.created_at >= '2026-09-01';
这种情况下,需要同时分析orders上的过滤和排序需求,以及users.id的访问方式。如果orders首先过滤出大量数据,再逐行访问users,执行计划是否合理就需要通过rows、实际执行时间和EXPLAIN ANALYZE进一步验证。
对于MySQL优化器估算明显不准确的情况,还应该检查统计信息和数据分布,而不是简单认为“优化器选错索引”。数据不断变化以后,统计信息可能无法准确反映当前数据特征。必要时可以结合ANALYZE TABLE等手段重新评估统计信息,并重新观察执行计划。
对于已经上线的SaaS系统,EXPLAIN最好与慢查询日志和实际监控结合使用。慢查询日志告诉你哪些SQL真正消耗时间,EXPLAIN告诉你MySQL准备如何执行,而EXPLAIN ANALYZE则可以进一步帮助验证实际执行情况。三者结合起来,才是真正完整的性能诊断链。
在开发阶段,也不要等数据库已经出现几千万行之后才第一次使用EXPLAIN。开发人员可以在核心SQL完成以后立即检查执行计划,尤其是登录、订单、搜索、列表、后台管理、统计接口等高频查询。随着数据量增长,再用接近生产规模的数据进行压力测试,因为一条在测试环境几万行数据上看起来非常漂亮的SQL,到了几千万行以后可能完全是另一回事。
对于PHP+MySQL的SaaS系统,这一点尤其重要。应用代码本身可能只执行了几十行PHP,但一次API请求背后可能触发多个SQL。如果其中一个SQL进行了百万级扫描,单个请求的数据库成本就可能非常高。当并发量增加以后,真正先达到瓶颈的往往不是PHP,而是数据库CPU、Buffer Pool、磁盘I/O、锁竞争或者连接池。
所以,阅读EXPLAIN不能只寻找“ALL”三个字,也不能追求每一条SQL都变成const或ref。正确的思路应该是从访问路径开始,结合rows、实际返回数据量、过滤效率、索引选择、JOIN方式、排序和聚合成本,再通过实际执行时间验证。
如果一条查询从全表扫描变成了索引范围扫描,rows也从数百万下降到几千,P95延迟从几百毫秒下降到十几毫秒,而且数据库CPU和I/O同步下降,那么这才是有意义的优化结果。反过来,如果只是让EXPLAIN里的type从ALL变成range,却因为索引维护成本增加导致写入性能下降,那么这个优化可能只是把问题从SELECT搬到了INSERT和UPDATE。
EXPLAIN真正的价值,不是告诉开发人员哪个字段“红灯”、哪个字段“绿灯”,而是把MySQL内部的访问路径暴露出来。专业的数据库优化最终需要回答的是三个问题:数据库正在扫描多少数据,为什么必须扫描这些数据,以及能不能在不破坏其他业务性能的情况下减少这些工作量。
当这三个问题能够被EXPLAIN、EXPLAIN ANALYZE、慢查询日志和实际监控数据共同回答时,SQL优化才真正从“凭经验加索引”变成了可验证的工程工作。
MySQL EXPLAIN 怎么看?从执行计划判断查询是否存在性能问题
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP