三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL执行计划深度解析:从EXPLAIN输出到SQL性能调优实战

MySQL执行计划深度解析:从EXPLAIN输出到SQL性能调优实战

1. 项目概述:为什么执行计划是DBA和开发者的“透视镜”?

做后端开发或者数据库管理,最怕的就是线上慢查询。用户抱怨页面转圈圈,监控告警响个不停,你打开慢日志一看,一条SQL执行了十几秒,数据量也不大,索引也建了,可它就是快不起来。这时候,如果你还停留在“猜”和“试”的阶段——比如盲目地加个索引,或者把SELECT *改成具体字段——那效率就太低了,而且很可能治标不治本。真正的数据库性能调优,必须从理解数据库引擎的“思考过程”开始,而EXPLAIN命令输出的执行计划(Execution Plan),就是MySQL给你的一副“透视镜”。

简单来说,执行计划就是MySQL优化器对你提交的SQL语句进行“成本分析”后,决定的一套具体执行方案。它告诉你:这张表打算怎么访问(是全表扫描还是走索引?),多张表之间准备怎么关联(是嵌套循环还是哈希连接?),以及每个步骤预估要处理多少行数据。看懂它,你就能精准定位SQL的瓶颈到底在哪里:是索引没命中?是关联顺序不合理?还是临时表或文件排序拖了后腿?我处理过太多案例,一个看似复杂的性能问题,往往通过分析执行计划,调整一个索引顺序或改写一个查询条件,性能就能提升几十甚至上百倍。这份详解,就是带你学会使用这副“透视镜”,从被动救火转向主动优化。

2. 执行计划核心字段全解:读懂优化器的“体检报告”

拿到一份EXPLAIN SELECT ...的输出,面对十几列信息,新手容易眼花缭乱。我们把它看作一份数据库执行该SQL的“体检报告”,每个字段都揭示了健康状况的一个维度。下面我们逐项拆解,我会结合最常见的场景告诉你需要重点盯防哪些“异常指标”。

2.1 核心访问类型:type字段的奥秘

type字段是执行计划的“心脏指标”,它描述了MySQL决定如何查找表中的行。从最优到最差,常见的有:

  • system/const:最优级别。通常是通过主键或唯一索引进行等值查询,最多返回一行。比如SELECT * FROM user WHERE id = 1。看到这个,说明这部分查询已经优化到极致了。
  • eq_ref:在表连接时出现,对于前一张表的每一行,当前表都通过主键或唯一非空索引进行单行匹配。常见于PRIMARY KEYUNIQUE KEY的等值连接,性能极佳。
  • ref:最常用的高效访问类型。通过普通二级索引进行等值查询,可能返回多行。例如,在user表的name字段上有索引,查询WHERE name = ‘张三‘type就是ref。这是你设计索引时期望看到的结果。
  • range:利用索引进行范围扫描,如BETWEEN><IN()等操作。它只检索给定范围内的行,性能依然不错,但不如ref
  • index全索引扫描(Full Index Scan)。它遍历整个索引树来获取数据,虽然比全表扫描快(因为索引文件通常比数据文件小),但它依然是扫描了整个索引,当数据量大时效率不高。常见于覆盖索引但需要扫描大部分索引条目的情况。
  • ALL全表扫描(Full Table Scan)。这就是最需要警惕的“红色警报”。意味着MySQL将读取整张表的每一行来找到匹配的行。对于大表,这通常是性能灾难的根源。

实操心得:在优化时,我们的核心目标就是尽可能让type远离ALLindex,向refeq_ref靠拢。如果看到ALL,第一反应就是检查WHERE条件涉及的字段是否有合适的索引。

2.2 可能用到的索引与实际使用的索引:keypossible_keys

  • possible_keys:查询可能使用到的索引。优化器会根据查询条件和表结构,列出所有理论上可供选择的索引。如果这一列为NULL,那就要高度警惕了,说明你的查询条件没有合适的索引可用。
  • key:查询实际决定使用的索引。这是优化器基于成本估算后的最终选择。如果keyNULL,即使possible_keys有值,也意味着优化器认为使用索引的成本高于全表扫描,最终选择了全表扫描。

一个经典陷阱possible_keys有值,但keyNULL。这往往是因为:

  1. 索引选择性太差(比如在“性别”字段上建索引),优化器认为用索引回表查数据不如直接扫全表。
  2. 需要回表查询的数据量太大,成本估算后放弃了索引。
  3. 查询条件使用了函数或表达式,导致索引失效,如WHERE YEAR(create_time) = 2023

2.3 扫描行数与过滤比例:rowsfiltered

  • rows:MySQL优化器预估为了找到所需的行,需要扫描多少行记录。这是一个基于统计信息的预估值,不一定完全准确,但极具参考价值。一个步骤的rows值巨大(比如几十万、上百万),通常就是性能瓶颈点。
  • filtered:这是一个百分比,表示存储引擎层返回的数据,在经过服务器层WHERE条件过滤后,剩余数据所占的百分比。filtered值越低(比如10%),说明索引过滤性越好,需要传到下一层(如下一个连接表或客户端)的数据越少。

联合分析的价值rows*filtered可以估算出将要参与下一阶段操作(如连接操作)的行数。例如,驱动表rows=1000filtered=10%,那么对于被驱动表大约需要查询100次。如果这个乘积很大,就需要考虑优化索引或调整连接顺序。

2.4 额外信息:Extra字段里的“魔鬼细节”

Extra字段包含了执行计划的额外重要信息,很多性能问题都藏在这里。常见的关键值有:

  • Using index覆盖索引(Covering Index)。查询的列都包含在使用的索引中,无需回表查询数据行。这是性能最佳的情况之一。
  • Using where:表示服务器层在存储引擎返回行之后,又进行了额外的过滤。如果typeALLindex,同时出现Using where,通常意味着性能很差,因为所有行都被读取后再过滤。
  • Using temporary:表示为了执行查询,MySQL需要创建一张临时表来保存中间结果。常见于GROUP BYDISTINCT操作,且没有按索引顺序进行。这通常发生在磁盘上,速度很慢。
  • Using filesort:表示MySQL无法利用索引直接完成排序,需要额外的排序步骤。如果排序数据量很大,会在磁盘上完成,非常消耗资源。
  • Using join buffer (Block Nested Loop):表示连接查询没有使用索引,或者索引效率不高,MySQL使用了连接缓冲区来优化嵌套循环连接。这通常是一个需要优化的信号。

注意事项:看到Using temporaryUsing filesort,尤其是对于大数据量的查询,一定要重点分析。尝试通过优化索引(建立包含排序字段的复合索引)或改写查询来消除它们。

3. 执行计划实战演练:从诊断到开方

光说不练假把式。我们通过一个模拟的电商订单查询场景,来完整走一遍分析优化的流程。假设我们有两张表:

CREATE TABLE `orders` ( `id` bigint PRIMARY KEY, `user_id` bigint NOT NULL, `product_id` int NOT NULL, `amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL COMMENT '1待支付,2已支付,3已完成', `create_time` datetime NOT NULL, KEY `idx_user_id` (`user_id`), KEY `idx_create_time` (`create_time`) ); CREATE TABLE `users` ( `id` bigint PRIMARY KEY, `name` varchar(50) NOT NULL, `vip_level` tinyint DEFAULT 0 );

3.1 案例:一个慢查询的诊断

业务需求:查找在2023年国庆期间(10月1日至7日)下单且订单状态为“已完成”的VIP用户(vip_level >= 2)的所有订单详情,并按订单金额降序排列。

初始SQL可能写成这样:

EXPLAIN SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.create_time BETWEEN '2023-10-01' AND '2023-10-07 23:59:59' AND o.status = 3 AND u.vip_level >= 2 ORDER BY o.amount DESC;

执行EXPLAIN后,我们可能得到如下计划(关键部分示意):

idselect_typetabletypepossible_keyskeyrowsfilteredExtra
1SIMPLEoALLidx_user_id,idx_create_timeNULL1000001.00Using where; Using temporary; Using filesort
1SIMPLEueq_refPRIMARYPRIMARY133.33Using where

解读与问题定位:

  1. 驱动表选择:优化器选择orders表作为驱动表(第一行)。
  2. 灾难性的访问类型:对orders表的访问类型是ALL,即全表扫描,预估扫描10万行。这是首要性能瓶颈。
  3. 索引失效possible_keys列出了idx_create_time,但keyNULL。说明优化器没有使用时间索引。为什么?因为WHERE条件中还有o.status = 3,而create_timestatus是两个独立的单列索引。MySQL在大部分情况下只能选择一个索引使用(索引合并优化并非总是启用且高效)。这里它可能认为status字段选择性太差(大部分订单都是已完成),用哪个索引都要回表大量数据,不如全表扫描。
  4. 额外负担Extra列出现了Using temporary; Using filesort。因为ORDER BY o.amount DESC,而amount字段上没有索引,且排序是在连接和过滤大量数据后进行的,导致需要临时表和文件排序,雪上加霜。
  5. 连接过程:对于orders表扫描出的每一行(经过WHERE过滤后剩余的行),再去users表通过主键(eq_ref)查找,效率尚可,但驱动表扫描行数太多,导致总的连接次数依然惊人。

3.2 优化方案设计与实施

针对以上诊断,我们可以开出如下“药方”:

第一步:为驱动表创建最有效的复合索引我们的查询条件在orders表上是create_time(范围)和status(等值)。根据最左前缀原则等值条件优先于范围条件的经验,创建一个复合索引:(status, create_time)。这样,优化器可以快速定位到所有status=3的记录,再在这些记录中按create_time范围筛选,效率远高于全表扫描。

ALTER TABLE orders ADD INDEX idx_status_createtime (status, create_time);

第二步:考虑覆盖索引,避免回表我们的查询选择了o.*,如果orders表字段很多,回表代价大。如果这个查询非常高频,可以考虑创建一个覆盖索引,将查询所需的列(特别是amount用于排序)包含进来。但注意,amountDESC排序,而create_time是范围查询,直接加在索引后面可能无法用于排序。更优的设计是(status, create_time, amount),这样在status=3create_time在某个范围内的数据,其amount在索引中已经是按顺序存储的了,可以避免filesort。但需要权衡索引大小。

-- 如果以排序优化为首要目标,可以尝试 ALTER TABLE orders ADD INDEX idx_status_createtime_amount (status, create_time, amount); -- 但注意,范围查询create_time之后,amount在索引中可能不是严格有序的,需要再次验证效果。

第三步:优化连接条件与查询字段确保连接字段user_idu.id上有索引(已有主键和idx_user_id)。同时,避免SELECT *,只查询必要的字段,减少数据传输和内存占用。

优化后的SQL与执行计划:

EXPLAIN SELECT o.id, o.amount, o.create_time, u.name -- 只取必要字段 FROM orders o FORCE INDEX (idx_status_createtime_amount) -- 可强制索引进行验证 JOIN users u ON o.user_id = u.id WHERE o.create_time BETWEEN '2023-10-01' AND '2023-10-07 23:59:59' AND o.status = 3 AND u.vip_level >= 2 ORDER BY o.amount DESC;

优化后的执行计划,orders表的type很可能变为range,使用我们新建的复合索引,rows大幅下降,Extra中的Using filesort也有可能因为索引包含了amount而消失(如果MySQL选择利用索引进行排序)。

4. 高级技巧与深度避坑指南

掌握了基础解读和常规优化后,一些更深层次的问题和技巧能让你在复杂场景下游刃有余。

4.1 索引失效的常见陷阱汇总

除了上面提到的,还有这些坑需要避开:

  1. 隐式类型转换WHERE user_id = ‘123‘,如果user_id是整型,字符串‘123‘会被转换,导致索引失效。务必保持类型一致。
  2. 对索引列进行运算或函数操作WHERE LEFT(name, 1) = ‘A‘WHERE price * 2 > 100。索引保存的是列的原值,计算后的值无法使用索引。
  3. 使用OR连接非索引列WHERE a = 1 OR b = 2,如果ab是单独的索引,有时会触发index_merge优化,但效率通常不高。如果有一个字段没索引,整个条件可能失效。
  4. LIKE查询以通配符开头WHERE name LIKE ‘%张‘。因为索引是按照列值的前缀组织的,无法利用。
  5. 不符合最左前缀原则的复合索引:对于索引(a, b, c),查询条件WHERE b = 1 AND c = 2是无法使用该索引的。必须包含最左列a(或a的范围查询)。

4.2EXPLAIN ANALYZE:获取实际执行数据

MySQL 8.0.18引入了EXPLAIN ANALYZE,这是一个革命性的工具。它不仅展示优化器的预估计划,还会实际执行查询,并返回每个执行步骤的实际耗时和实际行数。

EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 3 AND create_time > ‘2023-01-01‘;

输出会包含类似以下信息:

-> Filter: (orders.status = 3) (cost=... rows=...) (actual time=0.100..120.500 rows=10000 loops=1) -> Index range scan on orders using idx_status_createtime ...

这里你能看到actual time(实际时间)和rows(实际返回行数),可以与预估的rows对比。如果actual rows远大于estimated rows,说明表的统计信息已经过时,可以使用ANALYZE TABLE命令来更新统计信息,帮助优化器做出更准确的判断。

4.3 连接查询的优化策略

  1. 小表驱动大表:这是连接优化的重要原则。在嵌套循环连接中,应该将结果集较小的表作为驱动表(外层循环),以减少内层循环的次数。优化器通常会尝试这么做,但你可以通过调整WHERE条件或使用STRAIGHT_JOIN来影响它。
  2. 确保被驱动表的连接字段有索引:这是保证连接效率的黄金法则。如果被驱动表(内层表)的连接字段没有索引,那么对于驱动表的每一行,都要对被驱动表做一次全表扫描,性能是灾难性的(type会显示ALLExtra可能出现Using join buffer)。
  3. 子查询 vs 连接:很多时候,使用JOIN比使用INEXISTS子查询更容易被优化。但并非绝对,具体需要看执行计划。MySQL对某些子查询(如相关子查询)的优化能力 historically 较弱,但在新版本中已大幅改进。

4.4 分区表与执行计划

对于分区表,EXPLAINpartitions列会显示查询涉及的分区。优化关键在于分区裁剪(Partition Pruning)。如果你的查询条件能定位到少数几个分区,那么执行计划可能只扫描这几个分区,而不是整个逻辑表。例如,按月份分区的订单表,查询某个月的数据,type可能是ALL,但rows只显示该分区的数据量,性能依然可以接受。要确保WHERE条件中包含分区键,才能触发分区裁剪。

5. 系统化调优工作流与工具集成

将执行计划分析融入日常开发运维流程,才能发挥最大价值。

5.1 建立性能分析闭环

  1. 监控发现:通过慢查询日志(slow_query_log)、性能模式(performance_schema)或APM工具定位慢SQL。
  2. 执行计划诊断:对抓取的慢SQL立即使用EXPLAINEXPLAIN ANALYZE进行分析,重点关注type=ALLrows过大、出现Using temporary/filesort等环节。
  3. 提出假设并验证:根据分析结果,提出优化假设(如增加某个复合索引、改写查询逻辑、调整表结构)。在测试环境或低峰期,实施变更并再次获取执行计划,对比优化效果。
  4. 上线与回滚:将验证有效的优化方案部署到生产环境。务必准备好回滚方案,因为索引变更或查询重写可能影响其他未知查询。
  5. 持续观察:优化后,持续观察该SQL的执行时间和资源消耗,确认优化效果稳定。

5.2 可视化工具辅助

对于复杂的执行计划,文本输出可能不够直观。可以利用一些工具:

  • MySQL Workbench:它的可视化解释功能可以将EXPLAIN输出渲染成树形图或流程图,清晰地展示各步骤的依赖关系和成本。
  • Percona Toolkit 中的pt-visual-explain:命令行工具,能将EXPLAIN输出转换为更易读的树状文本格式。
  • 线上工具:有些网站提供将EXPLAIN文本粘贴进去进行可视化展示的功能。

5.3 统计信息的重要性

优化器依赖表的统计信息(如索引的基数cardinality)来做成本估算。如果统计信息不准确(例如,一个大表刚经过大量删除或导入,统计信息未更新),优化器就可能做出错误的决定,比如该走索引却走了全表扫描。定期或在数据量发生重大变化后,对核心表运行ANALYZE TABLE table_name;来更新统计信息,是维持执行计划稳定的重要维护操作。

执行计划不是一门玄学,而是一项可以通过系统学习和大量实践掌握的硬核技能。它要求你对数据库底层数据结构(B+树)、索引原理、优化器的工作方式有基本的理解。每一次慢查询的解决,都是一次经验的积累。我的习惯是,对于任何上线的复杂SQL或核心接口的SQL,都先EXPLAIN一下,看看执行计划是否健康,把性能问题扼杀在摇篮里。久而久之,你甚至能在写SQL的时候,就预判出它的执行路径,从而写出天生高效的查询语句。这才是高级工程师和架构师应有的数据库素养。

← 返回列表