
1. 从“慢SQL”到“执行计划”为什么EXPLAIN是性能分析的起点在数据库运维和开发工作中最常听到的抱怨之一就是“这个查询怎么这么慢”。面对一个执行缓慢的SQL语句很多人的第一反应是去检查索引、怀疑网络或者抱怨数据量太大。然而在没有明确证据的情况下这些猜测往往让我们在优化道路上南辕北辙。这时EXPLAIN命令就是我们手中那把最精准的“手术刀”它能将一条SQL语句在数据库内部如何执行的“计划”清晰地展示出来让我们从“盲人摸象”变为“洞察秋毫”。EXPLAIN是SQL标准中的一个关键字在MySQL、PostgreSQL、MariaDB等主流关系型数据库中都有实现虽然具体语法和输出格式略有不同。它的核心作用就是模拟数据库优化器如何执行一条给定的SQL语句并输出其预估的执行计划而不会真正去执行这条语句。这就像在真正动工建造一座大楼前先拿到一份详细的施工蓝图。通过这份蓝图我们可以预见到查询会使用哪些索引、表之间如何连接、数据读取的顺序和方式、以及每个步骤预估需要处理多少数据量。理解EXPLAIN的输出是进行SQL性能调优的必备基础技能。无论是排查线上慢查询还是在开发阶段评估新功能的SQL性能掌握EXPLAIN都能让你事半功倍。它直接回答了性能问题的核心数据库到底在“想”什么它打算怎么“做”这份“计划”里哪个环节最可能成为瓶颈2. EXPLAIN输出结果逐字段深度解读不同数据库的EXPLAIN输出格式不同但核心思想相通。我们以最常用的MySQL为例其EXPLAIN输出包含多个关键字段。理解每个字段的含义是读懂执行计划的第一步。假设我们有一张用户订单表orders约100万行和一张用户表users约10万行执行一个关联查询EXPLAIN SELECT u.name, o.order_amount FROM users u JOIN orders o ON u.id o.user_id WHERE u.city ‘Beijing‘ AND o.create_time ‘2023-01-01‘ ORDER BY o.create_time DESC LIMIT 100;执行后我们可能会得到类似下面的输出为便于解释进行了简化合并idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra1SIMPLEuNULLrefPRIMARY,idx_cityidx_city102const5000100.00Using where; Using temporary; Using filesort1SIMPLEoNULLrefidx_user_id,idx_timeidx_user_id8test.u.id2033.33Using where下面我们来逐一拆解这些字段背后的故事。2.1 id、select_type与table查询的结构与顺序id这是一个序列号表示SELECT子句的执行顺序。id越大优先级越高越先执行。如果id相同则从上到下顺序执行。在上面的例子中id都是1说明这是一个简单的连接执行顺序就是从上到下先访问users表别名u再访问orders表别名o。select_type表示每个SELECT子句的类型揭示了查询的复杂程度。SIMPLE简单的SELECT查询不包含子查询或UNION。我们的例子就是这种。PRIMARY查询中最外层的SELECT或者包含子查询时最外层的那个。SUBQUERY在SELECT或WHERE列表中包含了子查询。DERIVED在FROM列表中包含的子查询MySQL会将其结果存储在临时表中也称为“派生表”。UNIONUNION中的第二个或后续的SELECT。UNION RESULTUNION操作的结果。table显示当前行正在访问哪张表。有时会是derivedN或unionM,N这样的格式表示这是一个临时表或合并结果。2.2 type访问类型——性能的“第一性原理”type字段是EXPLAIN中判断查询性能最关键的指标之一。它描述了MySQL决定如何查找表中的行从最优到最差大致排序如下system const eq_ref ref range index ALL。我们的目标是尽可能让查询的type值靠前。system / const最优级别。system是const的特例表只有一行。const表示通过主键或唯一索引的一次性查找最多返回一行。例如SELECT * FROM users WHERE id 1。eq_ref在连接查询中对于前表的每一行从当前表中只读取一行。通常出现在使用主键或唯一索引的等值连接。性能极佳。ref使用非唯一索引进行等值查找或者使用索引的最左前缀进行查找。这是非常常见且高效的访问类型。在我们的例子中users表通过idx_city索引查找city‘Beijing‘orders表通过idx_user_id索引查找对应的user_id都属于ref。range使用索引检索给定范围的行常见于BETWEEN、、、IN()等操作。例如WHERE create_time ‘2023-01-01‘如果走了索引就是range。index全索引扫描Full Index Scan。遍历整个索引树来获取数据虽然避免了全表扫描但通常需要读取整个索引当索引很大时效率也不高。ALL全表扫描Full Table Scan。性能最差意味着MySQL需要逐行检查整张表来找到匹配的行。这是需要重点优化的信号。在我们的例子中users表的type是reforders表的type也是ref说明两者都高效地使用了索引进行等值查找这是一个良好的开端。2.3 possible_keys、key、key_len与ref索引的使用细节possible_keys显示查询可能使用到的索引。这是一个备选列表优化器会从中选择它认为成本最低的一个。如果此列为NULL则没有可用的索引。key优化器实际决定使用的索引。如果为NULL则表示未使用索引。这是需要重点关注的地方。有时possible_keys有值但key为NULL这通常意味着虽然存在索引但优化器认为全表扫描的成本更低例如表很小或者索引选择性极差。key_len表示优化器使用的索引的长度字节数。通过这个值我们可以判断索引是否被完全利用即“最左前缀原则”。计算方式取决于字段类型和字符集。例如一个int类型为4字节一个varchar(255)使用utf8mb4字符集如果字段可为NULL则需要额外1字节长度前缀需要1或2字节。分析key_len可以帮助我们确认复合索引中哪些部分被用上了。ref显示将哪个列或常量与key列中指定的索引进行比较以从表中选择行。常见的有const常量值、func函数值、其他表的列名。在我们的例子中users表的ref是const说明是用常量‘Beijing‘去匹配索引orders表的ref是test.u.id说明是用users表的id列去匹配索引。2.4 rows与filtered数据量的预估与过滤rowsMySQL优化器预估为了找到所需的行需要读取的行数。这是一个基于统计信息的估算值不一定精确但能反映大致的规模。在例子中优化器预估需要从users表读取5000行所有北京用户然后为这5000个用户中的每一个从orders表读取约20行rows: 20因此总的读取行数估算约为5000 * 20 100,000行。filtered这是一个百分比表示存储引擎层返回的数据在Server层经过WHERE条件过滤后剩余的行数所占的百分比。filtered值越大越好。在例子中users表的filtered是100%说明索引idx_city已经完美筛选出了所有city‘Beijing‘的行Server层无需再过滤。而orders表的filtered是33.33%这意味着通过idx_user_id索引找到的每个用户的订单大约只有1/3满足create_time ‘2023-01-01‘这个条件Server层还需要过滤掉另外2/3的数据。这个字段对于判断索引的有效性尤其是复合索引的设计非常有价值。2.5 Extra额外的执行信息——“魔鬼在细节中”Extra列包含了不适合在其他列显示但非常重要的额外信息。这里常常藏着性能问题的“元凶”。Using index表示查询使用了覆盖索引Covering Index即所有需要的数据都可以从索引中取得无需回表查询数据行。这是非常理想的情况。Using where表示Server层在存储引擎返回行之后又应用了额外的WHERE条件进行过滤。这说明索引可能没有完全覆盖查询条件。在我们的例子中orders表就有Using where因为create_time条件未被索引完全覆盖假设idx_user_id只是单列索引。Using temporary表示MySQL需要创建一张临时表来处理查询常见于GROUP BY和ORDER BY子句作用于不同的列时。这通常涉及磁盘IO对性能影响较大。例子中users表出现了这个提示。Using filesort表示MySQL无法利用索引完成排序需要额外的排序步骤。当排序数据量很大时在磁盘或内存中排序会消耗大量资源。例子中users表也出现了这个提示结合Using temporary说明ORDER BY o.create_time导致了性能开销。Using join buffer (Block Nested Loop)表示连接查询时被驱动表没有有效的索引可用MySQL会使用连接缓冲区来批量处理。这通常意味着需要为被驱动表的连接字段添加索引。3. 实战演练从EXPLAIN输出定位典型性能问题读懂字段只是第一步更重要的是将字段组合起来形成对查询性能的完整诊断。让我们结合上面的例子模拟一次完整的性能分析过程。第一步整体评估访问类型type我们看到两个表的type都是ref这很好说明连接都通过索引完成避免了全表扫描ALL这种最坏情况。第二步审视索引使用情况keyusers表使用了idx_city索引orders表使用了idx_user_id索引。但这里有一个潜在问题查询条件中还有o.create_time ‘2023-01-01‘而orders表使用的idx_user_id索引并不包含create_time字段。这意味着通过索引找到user_id匹配的行后需要回表到主键索引去取出完整的行数据然后再用WHERE条件中的create_time进行过滤这解释了Extra中的Using where和filtered33.33%。第三步分析数据扫描量rows预估扫描总行数约10万行。对于最终只取100条结果LIMIT 100的查询来说这个扫描比例是否合理这取决于北京用户数和他们的订单分布。如果北京用户只有5000但每个用户平均有几百个订单那么这个扫描量是不可避免的。但如果北京用户有10万那么这个查询就只扫描了其中5%的用户效率尚可。第四步揪出额外开销Extra这里发现了明显的性能瓶颈信号Using temporary; Using filesort。为什么会出现Using temporaryUsing filesort我们的查询有ORDER BY o.create_time DESC。由于orders表是先通过user_id索引访问获取到的行在create_time上很可能是无序的。为了得到按create_time排序的前100条结果MySQL不得不将所有满足WHERE条件的结果集收集起来放入一个临时表然后进行排序。当中间结果集很大时比如几十万行这个操作会非常慢。综合诊断与优化思路这个查询的瓶颈不在于数据查找ref类型效率不错而在于排序。优化目标是消除Using filesort和Using temporary。优化方案A为排序字段创建复合索引我们可以为orders表创建一个新的复合索引(user_id, create_time)。这样对于某个特定的user_id其对应的订单在索引中已经是按create_time排序的了。优化器可能会选择这个索引这样在查找数据的同时数据就已经是部分有序的按user_id分组组内按create_time排序可能可以避免全量排序。但注意这仍然需要合并多个用户组的结果进行全局排序。优化方案B改变查询逻辑更优更根本的优化是重新思考业务逻辑我们真的需要先连接所有北京用户的订单再从海量结果中排序取前100吗或许我们可以先找到最近下了订单的100个北京用户。 我们可以尝试使用子查询或INNER JOIN的变体但一个更有效的模式可能是利用“延迟关联”SELECT u.name, o.order_amount FROM ( SELECT id, order_amount, user_id FROM orders WHERE create_time ‘2023-01-01‘ ORDER BY create_time DESC LIMIT 1000 -- 先取一个稍大的范围确保能覆盖100个北京用户 ) o JOIN users u ON o.user_id u.id WHERE u.city ‘Beijing‘ ORDER BY o.create_time DESC LIMIT 100;在这个写法中内层子查询先在orders表上利用create_time的索引如果存在快速排序并取回1000个最近的订单ID和用户ID然后再与users表关联并过滤城市。这极大地缩小了排序和连接的数据集。当然这需要为orders.create_time建立索引并且1000这个魔数需要根据业务数据分布进行调整。注意优化没有银弹。方案B虽然可能大幅提升这个特定查询的速度但它改变了结果集的确定性依赖于内层LIMIT。在实际应用中必须结合业务逻辑的容忍度来权衡。这也正是EXPLAIN的价值所在它让我们清晰地看到不同写法的代价从而做出明智的选择。4. 进阶结合其他工具与真实场景的避坑指南仅仅看懂EXPLAIN的输出还不够在真实的生产环境中我们还需要将其与其他工具和上下文结合并避开一些常见的误区。4.1 EXPLAIN ANALYZE从“预估”到“实测”MySQL 8.0.18及以上版本提供了EXPLAIN ANALYZE命令。它与EXPLAIN的关键区别在于它会实际执行一遍查询然后给出真实的执行时间、实际扫描行数等指标与优化器的预估进行对比。EXPLAIN ANALYZE SELECT u.name, o.order_amount FROM users u JOIN orders o ON u.id o.user_id WHERE u.city ‘Beijing‘ AND o.create_time ‘2023-01-01‘ ORDER BY o.create_time DESC LIMIT 100;输出会包含类似这样的信息- Limit: 100 row(s) (cost1000.31 rows100) (actual time15.671..15.678 rows100 loops1) - Nested loop inner join (cost1000.31 rows100) (actual time15.670..15.676 rows100 loops1) - Filter: (u.city ‘Beijing‘) (cost505.15 rows5000) (actual time0.100..5.231 rows4800 loops1) - Index scan on u using idx_city (cost505.15 rows5000) (actual time0.098..4.123 rows5000 loops1) - Filter: (o.create_time ‘2023-01-01‘) (cost0.25 rows0) (actual time0.002..0.002 rows0.2 loops4800) - Index lookup on o using idx_user_id (user_idu.id) (cost0.25 rows20) (actual time0.001..0.001 rows20 loops4800)这里我们可以看到actual time实际时间、actual rows实际行数。对比rows预估行数和actual rows如果差异巨大说明优化器的统计信息可能已经过时需要考虑运行ANALYZE TABLE来更新统计信息以便优化器做出更准确的判断。4.2 常见误区与避坑要点“typeindex就一定比ALL好吗”不一定。index是全索引扫描ALL是全表扫描。当需要读取的数据量占表的大部分时比如超过30%全索引扫描可能因为要遍历整个索引树索引文件通常比数据文件小但也要看具体字段并频繁回表反而比直接全表扫描更慢。优化器通常会做出正确选择但如果你发现一个查询走了index但很慢可以尝试用FORCE INDEX/USE INDEX提示或者检查是否真的需要SELECT *能否改为覆盖索引。“possible_keys有值但key是NULL是不是优化器傻了”不一定。优化器基于成本Cost做决策。成本包括IO成本和CPU成本。如果它认为使用索引需要回表查询大量数据行其成本高于直接顺序扫描整张表特别是当表很小或者索引的选择性非常差时它就会选择全表扫描。此时不要盲目责怪优化器而应该思考这个索引的区分度选择性高吗查询是否真的需要返回这么多数据能否通过优化查询条件或使用覆盖索引来降低回表成本“Extra里出现Using filesort就一定要优化掉吗”视情况而定。如果排序的数据集很小比如几百行在内存中快速排序开销可以忽略不计。只有当排序数据集很大导致磁盘文件排序Using filesort可能涉及磁盘临时文件时才成为严重瓶颈。优化目标是减少需要排序的数据量或者利用索引的有序性来避免排序。“为什么我的查询在测试环境很快上线就慢”数据量差异是关键。EXPLAIN中的rows是基于统计信息估算的。测试环境数据量小分布均匀优化器选择的计划可能最优。生产环境数据量大分布可能倾斜例如90%的数据都集中在最近一个月导致同样的执行计划效率骤降。定期更新统计信息ANALYZE TABLE并在性能测试时使用贴近生产的数据样本至关重要。工具使用误区正如网络热词中提到的“dbeaver explain 显示的是个统计,没看到执行计划”一些图形化数据库工具如DBeaver的EXPLAIN可视化功能可能只展示了部分概要信息如成本、扫描行数而没有展示完整的执行计划树。当进行深度性能分析时务必使用数据库原生的命令行或能显示完整EXPLAIN格式的工具确保你能看到type、key、Extra等所有关键字段。4.3 系统化性能分析流程在实际工作中面对一个慢查询我个人的习惯性分析流程是抓取问题SQL从慢查询日志slow log、APM监控或数据库性能视图如information_schema.PROCESSLIST中定位到具体的慢SQL。基础EXPLAIN分析在测试环境或从库上执行EXPLAIN快速查看访问类型、索引使用、预估行数、额外信息定位最明显的瓶颈如ALL扫描、filesort。上下文还原确认SQL的执行环境数据库版本、表结构、索引情况、数据量级。深入EXPLAIN ANALYZE如果版本支持获取实际的执行数据验证预估的准确性。针对性优化根据分析结果采取相应措施增加缺失索引、优化现有索引调整顺序、改为覆盖索引、重写SQL如拆分复杂查询、使用派生表/临时表优化JOIN或GROUP BY、调整查询逻辑。验证与回滚在测试环境充分验证优化后的SQL确保结果正确且性能提升。记录变更并准备回滚方案。性能调优是一个迭代和权衡的过程。EXPLAIN命令提供了强大的洞察力但它给出的是一份“计划”。结合真实的数据分布、业务逻辑和数据库系统的特性才能做出最有效的优化决策。掌握它你就掌握了打开数据库黑盒的第一把钥匙。