资讯详情

从执行计划看懂SQL调优:一次慢查询优化的完整实战

📅 2026/10/15 5:09:47 | 华诺云谱 👁 阅读
从执行计划看懂SQL调优:一次慢查询优化的完整实战
聊起SQL调优很多一线研发团队其实早就形成了一套标准动作先把慢查询日志打开把那些跑了几秒甚至几十秒的SQL捞出来再用EXPLAIN看执行计划最后针对计划里的全表扫描、文件排序去补索引、改SQL。三步走完一条慢SQL从定位到优化落地往往不超过半小时。这中间最核心、也最值得花时间掌握的就是执行计划分析——它能让你在几秒内判断一条SQL到底是缺索引、关联方式选错还是查询逻辑本身就有问题。我见过不少业务同事刚开始接触调优时习惯把慢SQL甩给DBA或者到处找人问其实只要你学会看执行计划里的几列关键信息很多问题的答案自己就能读出来问人的时候也能问到点子上。这篇文章会围绕执行计划分析展开用一个非常典型的电商查询场景从头到尾走一遍完整调优流程从开启慢日志、定位问题SQL到EXPLAIN分析、设计索引再到优化前后的执行计划对比。内容面向有一定SQL基础、但还没系统接触过执行计划的开发者也适合那些已经会用EXPLAIN但停留在看到typeALL就知道要加索引层面的朋友——你会在后面的避坑章节里看到不少执行计划不会直接告诉你、但线上实战里非常要命的细节。1. 为什么慢SQL看了执行计划就能定位很多人第一次接触EXPLAIN下意识会觉得这是某个高深工具。其实它的本质特别朴素一条SQL交给MySQL之后优化器要先决定按什么顺序访问哪些表、用哪个索引、大概读多少行数据这个决策过程的结果以一张表的形式返回给你就是执行计划。换句话说执行计划是MySQL对这条SQL怎么做的一个自白书分析它你看到的不是结果对不对而是过程慢不慢、慢在哪。1.1 执行计划到底在表达什么我把执行计划理解成一份开车导航路线图。SQL是目的地优化器是导航引擎执行计划就是它给出的路线方案。同一段路可能有不同走法走高速索引扫描快但有限制条件走国道全表扫描什么车都能走但慢中途可能还要绕道文件排序、临时表。EXPLAIN把路线摊开给你看哪一段是高速、哪一段是土路、哪一段在绕圈子全都能在计划里找到对应标记。所以分析执行计划的第一步不是背字段而是建立一种直觉这条SQL当前是有索引可用且用上了还是明明有索引却没用还是压根没有可用索引只能全表扫。这三种情况对应的优化动作完全不同而判断依据就是type、key、rows、Extra这几列。把这个直觉训练出来你会发现自己看慢SQL的速度快得惊人。1.2 一线团队为什么都推崇这条调优链路大厂都在用这个说法核心指的并不是某个神秘商业工具而是一套被反复验证过的协作方法慢日志负责发现问题EXPLAIN负责定位病灶索引设计负责解决问题再用EXPLAIN验证问题是否真的解决。四个环节形成一个闭环任何人都能按照这个流程独立完成调优不需要碰运气。我之前在一个项目里带过几个刚转岗过来的同事他们没有专职DBA支持靠的就是这套流程。第一次遇到线上接口超时按流程打开慢日志发现一条订单列表查询跑了3秒钟EXPLAIN一看typeALL、rows二十多万Extra里还有Using filesort和Using temporary。顺着执行计划的信息给查询条件补了一个联合索引2分钟改完线上耗时从3秒降到200毫秒。整个过程没有用到任何商业工具也没有经验玄学就是老老实实看执行计划、对症下药。这也是为什么我会把看懂执行计划列为SQL调优第一优先级技能——它是所有后续操作的基础。2. 执行计划5个关键列一眼看穿SQL真实成本EXPLAIN返回的结果集一共有几十个字段但日常调优真正高频使用、而且信息量最大的基本集中在5列type、key、rows、filtered、Extra。看懂这5列就能覆盖绝大多数慢SQL的初始判断。2.1 最该先看的type列访问方式决定性能级别type列描述的是MySQL如何查找数据行从好到差大致是system const eq_ref ref range index ALL。实际业务中你看到最多的是ref、range、index、ALL这四个。ref使用非唯一索引或前缀索引查找一次索引查找能命中多行这是索引用对了的典型标志。range索引范围扫描比如WHERE里带BETWEEN、、、IN这类条件用索引去圈定一个范围。index全索引扫描从索引头扫到尾相当于把整棵索引树遍历一遍。这虽然用了索引但成本往往不低。ALL全表扫描这是最需要警惕的级别。执行计划里出现ALL意味着MySQL打算把整张表的数据一行一行读出来过滤数据量一大慢是必然的。我自己看执行计划的习惯是第一眼先扫type列如果看到ALL先别管别的列这条SQL大概率就是缺索引或者索引失效的问题如果看到index需要注意它是不是在扫描一棵很大的辅助索引如果看到ref或range说明访问路径本身是健康的问题可能要往排序、临时表、或者返回行数太多的方向排查。2.2 索引利用度与成本估算key、rows、filteredkey列告诉你这条SQL最终选了哪个索引没有走索引时这一列是NULL。它和type是配套的typeref但keyidx_name说明这个索引确实被用上了typeALL且key为NULL说明优化器找不到可用索引或者找到了但觉得用了不如不用。rows列表示优化器估算的需要扫描的行数注意是估算值不是实际扫描行数。它体现的是这条SQL大概要摸多少行数据数值越小通常越好。filtered表示经过WHERE条件过滤后剩余行数占扫描行数的百分比。filtered100说明过滤前后行数没变化filtered10说明扫描100行最终只留下10行。我举一个真实观察某条SQL的rows是20万filtered却是5%说明它扫描了20万行但最终有用的只有1万行。这种情况往往是索引选择不合适让MySQL在一个过大的范围内做过滤优化方向是让rows降下来、filtered升上去。rows和filtered结合起来看能判断扫描的代价和筛选的效率是否匹配。注意rows是CBO基于成本的优化器拍脑袋估出来的它依赖统计信息而统计信息不一定实时准确。千万不要把rows当成实际行数去写监控告警它只适合做趋势判断和方案对比。2.3 Extra列里的隐藏信号Extra列是执行计划里信息最丰富、但也最容易被忽略的一列。它不会说你错了而是用几个固定短语暗示执行过程中额外做了什么。我最关注三个信号。Using filesort表示MySQL需要额外做一次文件排序来满足ORDER BY。它不代表真的在磁盘上建了文件而是说没法直接利用索引顺序取数得自己排一遍。一条SQL如果出现filesort且数据量大排序就是主要瓶颈。优化方式通常是让排序字段进入索引由索引天然保证顺序。Using temporary表示执行过程中创建了临时表常见于GROUP BY、DISTINCT、多表关联去重等场景。临时表可能落在内存也可能落到磁盘落到磁盘就会明显变慢。出现这个信号时我会重点检查是不是可以改写SQL减少去重和分组或者让分组字段进入索引。Using index是最好的一种信号表示覆盖索引生效查询需要的字段都在索引里不需要回表取数据行。覆盖索引是SQL调优里性价比非常高的优化手段后面我会专门展开讲一次实际案例。3. 完整实操从慢日志到索引优化的全流程说再多不如跑一遍。这一节我用一个典型的商户订单查询场景分别演示优化前和优化后的执行计划让你直观看到一条SQL是怎么从全表扫描一步一步变成索引高效命中的。3.1 场景建模与慢SQL复现模拟项目X里有一张订单表结构大致如下CREATE TABLE order_info ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint NOT NULL, seller_id bigint NOT NULL, amount decimal(12,2) NOT NULL, order_status tinyint NOT NULL, create_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB;表中有一百多万条测试数据。业务反馈说某个商家订单查询接口在晚高峰超时定位到一条慢SQL需求是查某个店铺最近一周的订单列表按下单时间倒序分页返回。SELECT order_no, amount, order_status, create_time FROM order_info WHERE seller_id 10086 AND create_time 2024-11-01 00:00:00 AND create_time 2024-11-08 00:00:00 ORDER BY create_time DESC LIMIT 20;这条SQL表面看没有任何问题条件清晰、字段干净。第一次EXPLAIN之前我甚至怀疑是不是分页深翻页的问题结果一分析问题比想象的直接。3.2 第一次EXPLAIN看问题出在哪在SQL前面加上EXPLAIN关键字直接执行EXPLAIN SELECT order_no, amount, order_status, create_time FROM order_info WHERE seller_id 10086 AND create_time 2024-11-01 00:00:00 AND create_time 2024-11-08 00:00:00 ORDER BY create_time DESC LIMIT 20;执行计划结果如下idselect_typetabletypekeyrowsfilteredExtra1SIMPLEorder_infoALLNULL12000000.01Using where; Using filesort看到这张表我的判断流程是这样的typeALL说明走了全表扫描意味着这张表目前只有主键索引WHERE条件里的seller_id和create_time都没有索引可用key为NULL印证了这个判断rows是120万意味着MySQL计划把整张表扫一遍虽然filtered0.01看起来过滤效果好但这是在扫了120万行之后才过滤出来的结果Extra里还带着Using filesort说明ORDER BY create_time还得额外排序。全表扫描加上文件排序两个慢的因素叠加这条SQL在百万级数据量下跑出好几秒完全正常。优化方向也就明确了给seller_id和create_time建一个联合索引让过滤条件走索引同时让排序也能利用索引顺序。3.3 索引设计与前后对比从全表扫描到ref/range的蜕变这里有一个非常关键的判断点联合索引的字段顺序怎么排。WHERE条件是seller_id等值加create_time范围ORDER BY是create_time倒序。按照最左前缀原则我把等值条件字段seller_id放前面把排序字段create_time放后面建立索引ALTER TABLE order_info ADD INDEX idx_seller_create (seller_id, create_time);建完后重新执行EXPLAINidselect_typetabletypekeyrowsfilteredExtra1SIMPLEorder_inforangeidx_seller_create3200100.00Using index conditiontype从ALL变成了range说明这次不是全表扫描了而是通过索引去圈定范围rows从120万直接降到3200扫描量缩小了将近400倍这是性能提升最核心的来源Extra里的Using filesort也消失了因为ORDER BY create_time正好命中索引的第二个字段MySQL可以顺着索引顺序直接取数排序被索引天然解决。这条SQL的真实耗时从优化前的2.8秒降到了220毫秒虽然没有夸张到数10倍那么悬殊但也已经是超过一个数量级的提升。如果数据量到千万级这个差距还会进一步拉开。执行计划前后对比清楚地说明一切的优化效果都体现在type、rows、Extra这三个字段的变化上。注意索引不是越多越好。联合索引idx_seller_create虽然把查询和排序都覆盖了但如果业务里还有大量按user_id单独查询的场景就得额外评估是否单独建user_id索引。加索引前先看这个索引到底服务哪些高频SQL避免出现为一条SQL建一个索引的过度设计。4. 避坑实录执行计划不会告诉你的5个细节执行计划能帮你定位80%的问题但剩下20%的坑它不会直接告诉你。这些细节来自我在多个项目里的真实排障记录每条都对应过线上事故或者反复调优的教训。4.1 联合索引的最左前缀与顺序选择很多初学者加索引的时候完全不思考字段顺序今天看这个查询慢就建一个含这个字段的索引明天看那个查询慢再建一个。结果就是一张表五六个索引每个索引还有三四列写入性能被拖累查询也没快到哪里去。最左前缀原则其实很好理解联合索引(a, b, c)只能用来加速a、a,b、a,b,c这三种条件的查询想从b或者c开条件索引就用不上。所以设计联合索引时第一要务是找到出现频率最高的等值条件字段放最前面范围条件放后面。如果排序字段也想靠索引最好放在等值条件之后、范围条件之前或者之后具体要看排序方向和数据分布。我之前遇到一个把范围字段放最前面的案例索引是(create_time, seller_id)查询条件是seller_idxxx AND create_timexxx执行计划显示typeindex走了全索引扫描。原因就是create_time作为第一列只能命中范围无法直接定位seller_idMySQL只好把整棵索引树扫一遍再过滤。调整顺序重建成(seller_id, create_time)后type立刻变成了range。4.2 rows是估算值别当实数用执行计划里的rows是优化器根据统计信息估算出来的不是真实的扫描行数。统计信息基于采样采样过时了rows就会失真。我见过一条SQL执行计划里rows显示8000实际却扫了80万行——原因是表数据发生了大量增删统计信息没有及时更新。真正要看准确行数可以在分析前先执行ANALYZE TABLE刷新统计信息或者在EXPLAIN后加FORMATTREE看更细节的信息。做方案对比时看rows的变化趋势完全够用但不要基于rows去做精确的容量评估。4.3 索引失效的场景远比想象的多有些SQL看起来有索引可用执行计划里type却是ALL这种情况基本可以归为索引没被用上。最常见的三种失效原因我都踩过。第一种是函数包裹。WHERE YEAR(create_time) 2024create_time列被函数包住索引自然失效。解决办法是改写条件让列独立在表达式一侧create_time 2024-01-01 AND create_time 2025-01-01。第二种是隐式类型转换。字段是varchar类型SQL里却传了数字或者字段是intSQL里却传了字符串。MySQL会统一转类型再比较导致索引失效而且这种问题很难从肉眼发现。第三种是LIKE查询以通配符开头。WHERE name LIKE %张%最前面的%让MySQL无法从索引树的某个锚点开始找只能全部遍历。如果业务确实需要这种模糊匹配通常只能走全文索引或者搜索引擎靠普通B树索引解决不了。4.4 深分页的优化LIMIT并不总是很贵执行计划显示索引都用上了type也在range级别但SQL还是慢这时候多半是分页太深了。LIMIT 100000, 20的意思是先扫描前100020行丢掉前100000行只返回最后20行。扫描的行数不少但执行计划的rows可能显示得很小因为优化器是按理想情况下估算的。深分页的常见优化方案有两种第一种是延迟关联先查出主键或唯一键再回表拿完整数据。原理是利用索引树上的主键定位比直接扫大范围数据行快得多把代价大的回表推迟到最后20行才做。第二种是记录上一页最后一条数据的某个有序字段值下一页用WHERE create_time 上页最小值来取跳过已经翻过的数据这个方案实际效果最好但需要业务配合改造。4.5 覆盖索引让查询彻底不回表Extra里的Using index指的是查询所需的所有字段都包含在索引中MySQL只需要扫描索引树就能返回结果完全不需要回表读取聚簇索引里的数据行。这个优化空间在大字段回表场景下特别明显。举个例子查询只需要order_no和amount两个小字段而创建的这个联合索引恰好包含这两个字段那么执行计划里就会看到Using index。回表开销虽然单次不大但百万行级别的扫描下每一次回表都可能触发额外的随机I/O累积起来就是秒级差距。所以在设计高频查询的索引时我会尽量把SELECT里出现的字段在不影响索引体积的前提下追加到索引末尾。这就是常说的覆盖索引。个人习惯每完成一个联合索引设计我都会检查一下这个索引能否覆盖线上Top 10高频查询的SELECT字段。能覆盖就尽量覆盖这是成本最低、收益最稳定的提速手段。5. 顺手整理一份执行计划排查速查表如果你刚接触执行计划记不住各种字段的优先级可以把下面这张表收藏起来。它是我在实际项目中反复用到的一张判断清单基本覆盖了慢SQL初始定位的常见路径。执行计划证据初步判断首选优化动作typeALL, keyNULL缺索引或索引失效检查WHERE条件字段是否有合适索引typerange/index用上了索引但扫描范围偏大收紧条件、调整联合索引字段顺序ExtraUsing filesort排序无法利用索引把ORDER BY字段加入索引ExtraUsing temporary分组去重产生临时表改写SQL、让分组字段进索引ExtraUsing index覆盖索引生效保持考虑是否可推广到更多高频SQLkey列与实际预期不符优化器选了另一个索引检查统计信息必要时用FORCE INDEX做对比这张表不解决所有问题但它能帮你把90%的慢SQL在几分钟内给出一个正确方向的判断。这几次调优经验下来我最深的体会是SQL优化的核心并不在于会背多少种奇技淫巧而在于你能不能把一个看起来玄乎的性能问题拆解成扫描方式、扫描范围、额外操作三个可以量化的点然后针对那个最痛的点下手。执行计划正是把这些信息摆在你面前的工具用熟了之后你会发现自己看慢SQL的心态会从慌变成稳。最后再分享一个小建议如果你所在的项目还完全没有建立SQL review机制不妨从今天开始每次上线前把最近一周的慢日志SQL批量跑一遍EXPLAIN把新出现的全表扫描和filesort提前拦在发布之前。这个习惯坚持三个月你会回来感谢它。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑