资讯详情

MySQL执行计划详解:从EXPLAIN字段到慢查询优化实战

📅 2026/10/19 11:11:08 | 华诺云谱 👁 阅读
MySQL执行计划详解:从EXPLAIN字段到慢查询优化实战
说实话干了这么多年后端每天跟慢查询打交道最常用的工具其实就是EXPLAIN。不管你是刚入门MySQL的新手还是已经被线上问题磨了好几年的老油条执行计划都绕不开。它就像一个医生手里的CT机你写一条SQL过去它能直接告诉你MySQL准备怎么跑这条语句——走哪个索引、扫多少行、有没有临时表、要不要排序。这些信息拿到手慢查询优化就有了靶子不至于瞎猜。这篇东西我会讲得比较细把执行计划里每个关键字段、每个核心概念、我在实战中怎么用、遇到过哪些坑都摊开来说。内容偏向进阶适合已经会写SQL、想搞懂MySQL到底怎么工作的同学。1. 执行计划到底是什么以及什么时候该用很多同学一听“执行计划”就觉得高大上其实它就是MySQL优化器对一条SQL做的“执行方案”。这个方案里包含了访问表的方式、使用的索引、预估扫描的行数、表之间的连接顺序等等信息。你通过EXPLAIN关键字就能把这份方案调出来看。使用方式非常简单在SQL前面加上EXPLAIN就行EXPLAIN SELECT * FROM order_info WHERE user_id 10086;MySQL会把执行计划以表格形式返回每一行代表一个查询步骤。多表Join的SQL会返回多行每行对应一个表或者一个子查询。知道什么时候用比较关键。我个人的习惯是新上线的重要查询上线前先跑一次EXPLAIN确认没有全表扫描或者文件排序。线上慢查询日志里出现频率高的SQLCOPY出来先加EXPLAIN分析。别人写了SQL让你review别只看逻辑对不对直接看执行计划更靠谱。索引加了没效果用EXPLAIN看看到底走没走索引。所以它不是出了故障才翻出来的“救火工具”而是日常开发和review流程里的常规武器。2. 带着执行计划走一遍每个字段到底在说什么EXPLAIN的输出有十几个字段。刚开始接触的时候最容易犯的错就是只盯着type和rows看忽略了其他字段。但优化做到后面你会发现key_len、Extra、filtered这些字段才是真正的信息金矿。2.1 id和select_type看清查询结构id是查询的编号数字越大越先执行。对于简单查询id都是1。多表连接Join时如果都是普通连接id也相同此时MySQL会按照表的读取顺序来执行从上往下看就行。select_type描述的是一条SQL中每个SELECT部分的类型。常见的包括SIMPLE普通查询没有子查询和UNION。PRIMARY最外层的SELECT。SUBQUERY出现在WHERE或SELECT列表中的子查询非FROM子句。DERIVEDFROM子句中的子查询也就是衍生表。UNIONUNION操作中第二个及之后的SELECT。DEPENDENT SUBQUERY依赖外层查询结果的子查询这种通常性能不好。举个简单例子EXPLAIN SELECT * FROM user_info WHERE id IN (SELECT user_id FROM order_info WHERE status 1);外层是PRIMARY内层是SUBQUERY。如果看到DEPENDENT SUBQUERY就要高度警惕了很可能意味着每查一行外层数据内层子查询就执行一次这是典型的性能杀手。2.2 type这个字段基本决定了查询快慢type是执行计划里最值得关注的字段之一描述了MySQL如何访问表数据。它从左到右、从好到差大致是systemconsteq_refrefrangeindexALL实际业务中绝大多数情况下你能碰到的最好结果是ref能走到range已经可以接受最怕见到的是ALL也就是全表扫描。const通过主键或唯一索引最多只能匹配到一行。比如WHERE id 100MySQL直接把常量丢进索引一次定位。eq_ref多表Join时被驱动表通过主键或唯一索引进行匹配每行只匹配一条。ref通过普通二级索引进行等值匹配可能匹配到多行。这是比较常见的优秀访问类型。range通过索引进行范围查询比如、、BETWEEN、IN。index全索引扫描也就是遍历整个索引树比全表扫描好一些但也不快。ALL全表扫描意味着没有合适的索引可用。平时被慢查询折腾的SQLtype一般都是ALL。出现这种情况时第一反应应该是看一下WHERE条件里的列是不是缺索引或者索引没生效。2.3 possible_keys和key索引到底用没用上possible_keys列出了查询中可能用到的索引优化器候选的key是最终实际选择的索引。有些时候possible_keys不为空但key是NULL说明明明有索引优化器却没用。造成这种现象的原因大概有三种选择性太差。比如性别字段上建了索引但某个性别几乎占了一半数据MySQL觉得走索引还不如全表扫描。索引列上有计算或函数。比如WHERE DATE(create_time) 2024-01-01索引列套了函数直接废掉索引。隐式类型转换。比如字符串列user_no没加引号WHERE user_no 12345MySQL可能放弃走索引。遇到key和possible_keys不一致的情况别急着骂优化器先检查这三种情况大概率能找到问题。2.4 key_len索引长度里的门道key_len表示MySQL实际使用的索引字节数。这个字段很多人只看个大概但它是判断联合索引“用到了第几列”的关键依据。举个例子。假设有一张表CREATE TABLE order_info ( id BIGINT PRIMARY KEY, user_id INT, status TINYINT, create_time DATETIME, KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行EXPLAIN SELECT * FROM order_info WHERE user_id 10086 AND status 1;key_len的值为5。为什么user_id是INT类型占4个字节加上可空标记占1个字节字段默认允许NULL时需要额外1字节合计5字节。这说明联合索引只用到了第一列user_id。如果key_len变成了6说明statusTINYINT占1字节可空标记1字节2字节也参与了匹配索引的两列都用上了。这个判断方式在处理联合索引时极其有用。你可以通过key_len精确知道SQL实际用了联合索引中的哪几列从而判断当前索引设计是否匹配查询。2.5 rows和filtered别全信但要注意趋势rows是MySQL估算的需要读取的行数filtered表示经过WHERE条件过滤后剩余行数的百分比。比如rows1000filtered10%意味着最终返回约100行。很多人过度依赖rows把它当成精确值。这里要提醒一下它是基于统计信息和索引基数的估算不是真实扫描行数。尤其当表数据分布不均、或者MySQL统计信息过期时rows的误差会很大。真正有价值的不是单看某一次rows数值而是看它的趋势。同样的SQL优化前rows是50万优化后变成500这就是非常直观的优化效果。2.6 Extra这里藏着MySQL的“潜台词”Extra可能是整个执行计划信息量最大的字段值得逐条琢磨。Using filesort需要额外排序操作。看到这个词基本可以认定为性能隐患。ORDER BY的列没有走索引MySQL只能把数据查出后在内存或磁盘上排序。如果是几万行还好几百上千万行就很要命。Using temporary使用了临时表。常出现在GROUP BY或DISTINCT子句执行过程中一般也意味着性能问题。Using index这是好事表示查询只访问索引不必回表覆盖索引。Using where表示在存储引擎层过滤后Server层还要再进行条件过滤。通常说明索引下推后还有WHERE条件落在普通列上。Using index condition索引下推ICP生效了MySQL会把部分WHERE条件下推到存储引擎层过滤减少了回表次数。Using join buffer被驱动表没走索引MySQL用join buffer做连接。会让性能直线下降。Extra的内容组合起来基本能还原MySQL执行时的“心路历程”。我在分析慢SQL时一半以上的判断结论都来自这个字段。3. 一个真实场景从执行计划到SQL重写光讲字段概念不够我拿一个自己处理过的经典场景来演示整个分析流程。假设业务上有张订单表数据量接近千万级别结构简化如下CREATE TABLE order_info ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_order_no (order_no), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;线上反馈某后台页面打开很慢定位到一条SQLSELECT * FROM order_info WHERE status 2 ORDER BY create_time DESC LIMIT 20;我第一时间执行计划拉出来看核心结果如下字段值typeALLpossible_keysNULLkeyNULLrows约932万ExtraUsing where; Using filesort出现typeALL、rows接近全表、Extra带Using filesort基本把慢查询的原因全占了。为什么status2没走索引因为status列本身没有独立索引。即便现建一个status单列索引以这个表的数据分布来看status2的订单可能占据了相当大的比例MySQL还是会选择全表扫描因为回表代价太大。真正的优化方向是让排序字段create_time参与索引匹配从而消除Using filesort。最终我选择建立联合索引ALTER TABLE order_info ADD INDEX idx_status_create_time (status, create_time);再跑执行计划结果变成字段值typerefkeyidx_status_create_timekey_len1rows约1.2万ExtraUsing index condition注意这里的key_len1因为status是TINYINT只占1字节。说明这次MySQL通过status等值匹配走了索引并且在索引内部就已经按create_time排好序了Extra里的Using filesort消失了。查询时间从原来的1.8秒降到40毫秒左右效果立竿见影。整个优化过程中执行计划最关键的信号就是那串ALL Using filesort。看到这两个组合脑子里就要立刻意识到索引缺失导致全表扫描排序也没走索引必须通过索引合并这两个消耗点。这个案例也说明一个道理很多时候不是SQL写错了而是索引设计没有跟上查询模式。执行计划就是用来验证这两者是否匹配的工具。4. 执行计划的进阶用法和排查技巧当基础字段都掌握了可以玩玩EXPLAIN的进阶姿势这些在定位复杂问题时非常有用。4.1 用FORMATJSON和FORMATTREE看更细的成本MySQL 8.0支持两种更直观的执行计划展示方式EXPLAIN FORMATJSON SELECT * FROM order_info WHERE user_id 10086; EXPLAIN FORMATTREE SELECT * FROM order_info WHERE user_id 10086;FORMATJSON会给出一个结构化的JSON输出里面包含query_cost信息可以直接看到优化器评估的IO成本和CPU成本。做方案对比时这个成本值可以作为量化参考。FORMATTREE的输出是树形结构能清楚看到表之间的连接顺序、哪个是驱动表、哪个是被驱动表。对于多表Join的场景这个格式对理解执行顺序帮助特别大。4.2 不要忽略EXPLAIN ANALYZEMySQL 8.0.18提供了一个更强大的工具——EXPLAIN ANALYZE。它不是估算是真的把SQL跑一遍输出实际执行时间和实际扫描行数EXPLAIN ANALYZE SELECT * FROM order_info WHERE user_id 10086;执行后输出类似这样- Filter: (order_info.user_id 10086) (actual time0.123..0.215 rows20 loops1) - Index lookup on order_info using idx_user_id (actual time0.098..0.190 rows20 loops1)这里的actual time和rows是真实的执行数据能帮你看清估算与实际之间的差距。线上排查时凡是遇到估算严重失真的情况用EXPLAIN ANALYZE一般能直接把真相拉出来。不过使用它要评估代价毕竟是真跑一遍SQL对于超大表的复杂查询建议在压测环境或低峰期使用。4.3 排查索引失效的几条自查路径执行计划里如果发现明明建了索引却没走我一般按顺序排查四件事第一看索引列上有没有函数或运算。WHERE SUBSTR(phone,1,3) 138这种写法直接废掉索引应该改成phone LIKE 138%。第二确认类型是否一致。字符串列查询不带引号数字列查询带了引号都会导致隐式类型转换让MySQL放弃候选索引。第三检查排序是否和索引顺序匹配。联合索引(a, b, c)你ORDER BY b是走不了索引的。如果ORDER BY a DESC, b ASC方向不一致也可能导致排序失效。第四看是不是统计信息太旧。表数据剧烈变化后如果还没有更新统计信息优化器可能做出错误的索引选择。运行ANALYZE TABLE 表名刷新一下统计信息再试往往能解决。4.4 关于深分页问题的额外提醒一条SQL如果Extra里只有Using wheretype也还不错但查询还是慢这时候大概率是LIMIT偏移量太大导致的。比如LIMIT 500000, 20MySQL要扫描前50万行再丢弃前面的代价极大。这类问题执行计划看不出来因为它展示的是访问路径不是行位移开销。解决手段是延迟关联或游标分页用ID做条件代替偏移量。这种场景下执行计划的作用变成了确认主查询走索引了但深分页的优化需要靠业务层SQL改造来完成。5. 常见问题速查表结合我日常回复同事和社区提问的经验把常见执行计划问题归纳成一张速查表症状可能原因解决思路typeALL无合适索引或选择性差加索引或改用覆盖索引ExtraUsing filesortORDER BY未走索引让排序列参与索引或改写SQLExtraUsing temporaryGROUP BY/DISTINCT未走索引优化索引或改写为JOINkeyNULL但有possible_keys函数/类型转换/选择性差消除索引列函数调整类型rows与预期差距极大统计信息过期ANALYZE TABLE刷新统计key_len短于预期联合索引未用全列审查查询条件与索引列顺序这张表几乎是日常DBA问题的浓缩版。遇到执行计划相关的疑惑对照表格基本能锁定大致方向。我自己在实际工作中还有一个习惯每定位到一个慢查询会把执行计划和最终优化方案记录在案定期复盘。时间久了哪些SQL写法容易踩坑、哪类索引设计存在误区心里都有谱。这算是在EXPLAIN之外更高一层的“执行计划思维”了。文章写到这里关于EXPLAIN的核心内容都覆盖了。如果你手头正好有慢SQL别犹豫先跑一下执行计划大部分问题自己就能看穿。优化的第一步永远是先看清MySQL到底在干什么。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑