资讯动态

MySQL ORDER BY排序:全字段排序与rowid排序底层原理与优化

发布时间:2026/10/13 4:29:23 来源:尧图企业网站定制
1. 排序被单独拎出来当面试题到底在考什么我面过不少候选人也帮别人模拟过面试MySQL这块如果只让选一个“看起来简单、翻车率却极高”的知识点ORDER BY的底层实现绝对排得上号。很多人能张口说出“全字段排序”“rowid排序”这两个词但再往深问一句“MySQL什么时候用哪种回表发生在哪一步sort buffer满了会怎样”就卡壳了。先说清楚这两个概念到底属于哪个层面。它们不是两种并列的索引结构也不是存储引擎层面的事而是优化器 执行器在处理ORDER BY时决定采用filesort排序算法的两种内部策略。注意filesort并不是“排序只能落到磁盘文件”它只是“需要额外排序、不能用索引顺序直接返回”的统称排序过程可能完全在内存里的sort buffer完成也可能溢出到磁盘临时文件取决于数据量。这个面试题的考查点很直接第一你知不知道ORDER BY不一定走索引而是可能走排序第二你知不知道即使走了排序内部还会根据代价分两条路第三你能否准确说出来这两条路的差异和触发条件。对应到实际工作是排查慢查询时EXPLAIN结果里出现了Using filesort你要能判断它到底在做什么、瓶颈在哪、怎么消除它。EXPLAIN SELECT id, name, age, city FROM user WHERE city 深圳 ORDER BY age;如果city上有索引但age上没有大概率能看到Using index condition; Using filesort。这时问题就来了这个filesort是全字段还是rowid它和sort buffer大小有什么关系下文一层层拆。2. 全字段排序先把要返回的字段全部请进sort buffer再动手排2.1 执行过程逐段拆解全字段排序在MySQL源码和日志里也常被称为“original filesort algorithm”老一辈DBA口中的“常规排序”指的就是它。核心思想非常朴素既然要排序那把最终要展示给用户的所有字段一股脑塞进排序内存在内存里排完直接返回后面不用再碰原表。拿上面那条查询举例假设user表结构包含id、name、age、city、address等字段查询只要id、name、age、city四列排序键是age。全字段排序的执行链路如下。第一步根据WHERE city深圳走二级索引找到所有满足条件的主键id。如果city上的索引是普通二级索引索引里不直接包含age、name等字段信息所以需要拿着这批主键回聚簇索引取整行数据这个过程叫回表。也就是说排序还没开始先回了一波表。第二步把取出来的每行中查询需要的字段id、name、age、city复制到sort buffer。这里有个容易被忽略的细节它拷贝的不是整行是SELECT清单里的字段包括排序键age。第三步在sort buffer里按age做快速排序。sort buffer是一块独立的内存区域通过参数sort_buffer_size控制默认值在MySQL 8.0里通常是256KB。如果所有要排序的数据能放进这块内存一趟排完快得很。第四步如果数据量超过了sort_buffer_size内存装不下MySQL会采用归并排序的思路先排出一批有序结果写到磁盘临时文件接着处理下一批最后把多个有序文件做多路归并。临时文件的IO次数直接取决于排序数据量大小和buffer大小。第五步排序结束把所有有序结果返回客户端。这条链路的关键点是整个过程中sort buffer里又有排序字段、又有返回字段排序结束后不需要再次回表直接拿数据走人。2.2 全字段排序的适用边界与“宽行”隐患全字段排序的优点是减少一次回表也就是少一次随机磁盘IO——如果排序结果被送到客户端前还得重新去聚簇索引按id取数据那代价明显更大。但反过来它也有个致命短板sort buffer的容量是固定的如果每条数据的“排序字段返回字段”越长能装进buffer的行数就越少。举个例子一张订单表SELECT order_id, order_no, user_id, amount, status, create_time, remark ... ORDER BY amount如果查询要求返回的字段特别多尤其remark这种VARCHAR(500)字段也被塞进排序缓冲区假设一行数据在buffer里占600字节256KB的sort buffer只能装400多行。稍微大点的分页查询直接就内存溢出走磁盘归并性能断崖式下跌。所以全字段排序并不是永远最优。当单行排序数据过大时聪明的优化器会换另一条路——只把必要的东西放进buffer排完再回头找整行。2.3 为什么会在sort buffer里存“多余的字段”这里有一个值得展开的底层原因sort buffer里存的字段本质上是执行器“预测”后面还会用到哪些列。它没法只存排序键因为一旦排完序要SELECT返回字段而这些字段又不在buffer里它就还得回表那就失去全字段排序的意义了。所以全字段排序的字典里返回字段越少越好。如果你只需要id和排序键那buffer里就只有两列能装的行数就多。这也是很多性能优化文章反复强调“SELECT *ORDER BY是大忌”的原因。SELECT *会把所有列拖进sort buffer排序性能和返回数据量双双恶化。实际调优时我看到不少人只盯着ORDER BY的索引忽略了返回列的数量结果加了索引也没快多少。3. rowid排序先搞定排序本身再按图索骥完成任务3.1 核心思想与执行过程rowid排序在源码里叫“modified filesort algorithm”翻译成大白话就是“先排序后回表”。它观察到全字段排序的一个痛点sort buffer被无谓的宽字段挤爆。于是它转变策略在sort buffer里只放两样东西排序字段比如age、amount和主键idrowid就是主键也可能是系统自动生成的行id。还是那条city深圳的查询rowid排序的执行链路是这样的。第一步和全字段排序一样先从二级索引拿到满足条件的主键id集合然后回表取行。注意这个“回表取行”和后面那个“回表”不同这次回表是为了拿到age字段值和主键id。其实更准确地说如果二级索引本身不带age这一步无论如何都要发生。第二步把age和id这两列放入sort buffer按age排序。因为每行数据很短同样256KB的buffer能装下的行数会多得多。比如每行只有8字节的id加4字节的age就算加上一些内部结构开销装几万行轻轻松松内存排序的概率大幅提升。第三步排序完成后手上得到的是一个“有序的(id, age)”列表但客户端要的是id, name, age, city所以必须拿着id再去聚簇索引回一次表把每一行的完整字段取出来按排好的顺序返回。rowid排序的关键词就是“多了一次回表”。它用一次额外的随机磁盘访问换取了sort buffer能装更多待排序行、减少磁盘临时文件归并的概率。如果你sort buffer本来就小或者行非常宽走全字段排序会导致大量磁盘IO这时rowid排序的“少几次归并多一轮回表”反而总体代价更低。3.2 回表的代价该怎么量化很多人对rowid排序“多回一次表”没有直观感觉我在这里给个简单测算。假设排序后有N行结果每行回表都是一次主键查找聚簇索引通常走B树级别大概3层前两层的索引页大概率在Buffer Pool里真正可能触发磁盘读取的是第三层叶子页。如果N比较大这个再乘以随机IO的耗时就是额外成本。但要注意如果在排序前那批回表已经发生过而且相关数据页还热乎着Buffer Pool命中率会比较高rowid排序的额外回表也没那么可怕。真正怕的是排序产生了临时文件再回表时数据页已经被换出这时每回一次都可能撞磁盘。从这个角度看全字段排序和rowid排序并不是绝对的“哪个更好”而是针对行宽和数据量的成本博弈。3.3 优化器怎么选sort buffer已定就不再只看心情MySQL优化器的选择逻辑比较简单粗暴核心依据是排序元组的长度是否超过某个阈值。每个排序元组包含排序字段、主键id加上必要的长度和指针信息。预估总数据量能装进sort buffer、且单个元组长度不长就选全字段排序否则选rowid排序。这个决策发生在执行计划生成阶段所以你用EXPLAIN只能看到Using filesort看不到具体是哪种模式。想验证当前跑的是哪种排序只能靠OPTIMIZER_TRACE这类手段去追踪优化器内部的成本决策过程后面第5章会写具体操作。提示在MySQL 8.0中旧的max_length_for_sort_data参数仍然存在但它控制的是“当行长达到多少时放弃全字段排序、改用rowid排序”的阈值判断逻辑。这个值默认是4096字节。如果你的排序行超过这个长度就会强制切到rowid排序。这个参数很容易被忽略但它特别能解释“为什么我改了sort_buffer_size还是没效果”这类问题。4. 全字段与rowid切换的真正临界点不只是sort_buffer_size4.1 参数组合如何共同决定排序策略面试官问“MySQL什么时候走全字段排序什么时候走rowid排序”如果只答“看sort_buffer够不够大”只能算及格。真正完整的答案涉及三个层面的判断。第一个层面是单行排序元组的预估长度。元组长度 排序字段长度 主键长度 若干字节的行指针/长度信息。这个值会和max_length_for_sort_data比较一旦元组长度大于阈值优化器直接放弃全字段排序。第二个层面是总数据量与sort_buffer_size的关系。即使元组长度不算大但如果满足条件的行数极多总大小会撑爆sort buffer这时元组短的优势就不存在了——因为无论如何都要产生磁盘临时文件。一旦走磁盘归并路径行宽的影响权重会下降此时MySQL倾向于选择rowid排序因为减少buffer占用可以降低归并轮数。第三个层面是是否涉及覆盖索引。如果ORDER BY的字段以及SELECT的字段全部包含在同一个二级索引里排序根本不需要回表直接走索引顺序扫描连filesort都不出现。这就绕开了两种排序之争。数据库规范里一直强调的“覆盖索引优化排序”本质就是在源头消除filesort。三个层面总结成一张决策表判断条件倾向的排序策略原因元组长度短且总量能进sort buffer全字段排序省一次回表内存排序快元组长度超过max_length_for_sort_data(4096)rowid排序防宽行撑爆buffer元组短但总量远超sort_buffer_sizerowid排序降低归并轮数减少临时文件IO排序字段和返回字段都在索引中不走filesort覆盖索引直接按序取数4.2 sort_buffer_size的理想取值该怎么定很多DBA喜欢把sort_buffer_size调得很大比如调到64MB甚至128MB。从实践角度看收益往往没有想象中高。原因很简单sort_buffer是每线程独立分配的不是全局共享。高并发下100个连接同时排序每个分配32MB就是3.2GB内存瞬间被吃掉还没算上其他buffer和连接本身的内存。MySQL 8.0里这个参数属于Per Session分配范畴连接断开才释放极高的并发场景配置过大反而成了内存杀手。合理做法是先观察状态变量Sort_merge_passes。如果它在繁忙时段持续偏大说明排序频繁落盘可以逐步调大sort_buffer_size如果这个值长期是0说明所有排序都在内存中完成加buffer没有意义。这个指标比“拍脑袋调大”靠谱得多。我见过一个真实案例某报表查询单条SQL每次排序约5万行每行约200字节总量10MB。默认256KB的buffer会触发几十次merge passSQL耗时2.7秒。把sort_buffer_size调到16MB之后merge pass直接降到0查询耗时降到0.6秒。后来并发上来了又发现这个查询一天被调用上千次于是改成了“查询内临时设置sort_buffer_size执行完恢复默认”而不是全局调大既保住了大部分收益也没有拖垮实例内存。-- 会话级调整示例 SET SESSION sort_buffer_size 16 * 1024 * 1024; SELECT ... ORDER BY ...; SET SESSION sort_buffer_size DEFAULT;如果面试被追问“sort_buffer_size越大越好吗”一定不要点头。正确的回答是要在单查询收益和并发内存开销之间做权衡观察Sort_merge_passes再决定。4.3 分页深翻页时两种排序策略的差异会更明显深分页场景是两种排序差异最容易暴露的地方。比如SELECT id, title, content, author FROM article ORDER BY publish_time DESC LIMIT 100000, 20;这条SQL最终只需要20行但排序过程要把前100020行的排序全部做完。如果走全字段排序content这种大字段会被大量复制进sort buffer即使最终只取20行前面的10万行也都占过内存。如果publish_time id两个字段长度很小全字段排序也许还能扛但是加上了content单行元组轻松超过4096字节优化器大概率切到rowid排序。这时候即便rowid排序前面的10万行也要先排出来再回表取20行整体成本依然高。更优的方案是改写成“延迟关联”加覆盖索引的思路先在索引里排好(publish_time, id)分页取出第100000到100020个id再回到原表去取这20行的完整数据。这是在后端开发中非常经典的分页优化技巧也顺便回答了面试官脑洞里的“那你们实际生产环境怎么做的”。-- 延迟关联写法示例 SELECT a.id, a.title, a.content, a.author FROM article a INNER JOIN ( SELECT id FROM article ORDER BY publish_time DESC LIMIT 100000, 20 ) t ON a.id t.id ORDER BY a.publish_time DESC;这里我建议你顺手记一个结论只要能用覆盖索引消除filesort就优先消除消除不了时先看sort buffer抢占情况再看是否压缩返回字段宽度。全字段还是rowid是优化器在特定参数下的既定选择而不是你在SQL里能直接“指定”的东西。你能操控的是让单行元组更小、让总量更少、让sort buffer更合理。5. 用OPTIMIZER_TRACE看清优化器到底选了哪条路5.1 排查步骤与方法既然EXPLAIN只看得到Using filesort看不到全字段还是rowid那就用OPTIMIZER_TRACE把优化器的小心思全抖出来。步骤很简单官方支持直接在一个会话里开traceSET optimizer_trace enabledon; SELECT id, name, age, city FROM user WHERE city 深圳 ORDER BY age; SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G重点看filesort_priority_queue_optimization、filesort_summary等字段。例如filesort_summary: { rows: 5120, sort_algorithm: std::stable_sort, sort_type: sort_key_and_rowid, sort_param: { sort_buffer_size: 262144, } }sort_type如果显示sort_key_and_rowid说明走的rowid排序如果显示sort_key_and_extra_fields或者看起来像sort_keydata就是全字段排序。看到这两个键名基本就实锤了优化器的选择。在MySQL 5.7及以后trace里还会暴露filesort_priority_queue_optimization相关的内容和堆排序优化有关。比如LIMIT 100这种需求并不需要全量排序它会用优先队列维护Top N内存占用远小于全排。这也是为什么深翻页场景要先彻底完成排序才能取边界数据机制上会吃亏。5.2 实际生产中的分析技巧实际生产环境不建议直接开全局optimizer_trace可以在会话级别只对出问题的SQL开启。如果SQL执行太久可以先把它的执行计划打出来确定确实是filesort在拖时间。还有个小技巧SHOW SESSION STATUS LIKE Sort%。查询之前先看Sort_merge_passes等状态值跑完SQL后再看增量能直接判断排序是否落盘、merge了几次。排查经验往往是这样的循环先确定排序确实存在 → 看merge passes判断落盘严重程度 → 用trace确认到底走了哪种排序 → 针对行宽或buffer大小调整 → 重新验证merge passes是否下降。这个循环做熟了比背一百个调优结论都有用。6. 面到这个题怎么把回答讲得像一线做过的6.1 递进式回答结构把这道面试题吃透之后真正上考场时要学会分层输出。一个推荐的回答结构是先讲结论再展开原理最后落到参数和实践。第一层一句话定性——“全字段排序和rowid排序都是filesort内部的两种实现策略区别在于sort buffer里放哪些字段、排完序之后是否还需要回表。”第二层分步讲过程——全字段排序把查询需要的所有字段放进sort buffer排完直接返回rowid排序只放排序字段和主键排完再回表取完整行。第三层给切换条件——单行元组长度超过max_length_for_sort_data或数据量远超sort buffer容量时优化器会倾向rowid排序元组短且能放进buffer时倾向全字段排序。第四层结合优化实践——实际操作中更推荐用覆盖索引消除filesort而不是纠结调哪种排序sort_buffer_size要参照Sort_merge_passes来决定不能盲目调大。这个结构的好处是无论面试官是想考你知识面还是想看你的实战思维你都有对应层次的内容。而且每层之间有时间线递进不是堆名词。6.2 几个容易被追问的延伸点这道题几乎必然会被追问的延伸方向提前准备好会让面试体验顺畅很多。第一个追问是“回表到底发生在哪里”。说清楚全字段排序的回表发生在排序前rowid排序的回表发生在排序后就能证明你真的理解执行顺序。顺便可以提一句如果二级索引是覆盖索引两种排序都可能直接消失。第二个追问是“如果LIMIT很小比如只要前10条还是先全排吗”。这里能扯出优先队列优化MySQL会用堆来维护Top N不需要把全部行排完再取前10。这个点对深分页和Top N性能差距的解释很有价值。第三个追问是“你们线上怎么避免filesort”。标准答案绕不开索引设计和覆盖索引但更出彩的是结合业务场景说读多写少的报表查询可以让排序走索引写多的大表则要权衡索引维护代价必要时接受小范围排序并用buffer兜底。提示回答时不要贬低任何一条路径。全字段排序“省一次回表”和rowid排序“省buffer空间”各有适用前提。表现出你在做取舍而不是念结论这个面试题才算真正过了。7. 从一道面试题到真实业务优化常见误区清单面试答得好不代表线上调优不会翻车。这里列几个我复盘过很多次的常见误区顺便给出对应的纠正思路。7.1 误区一一看到filesort就觉得必须干掉不是所有filesort都慢。数据量小、sort buffer够用时内存排序可能就是最优解。比如一个用户表按注册时间排序取最近登录的50条数据本来就不大强行加一个(regist_time)索引反而拖累写入。判断依据应该是merge passes有没有显著增长、单条SQL耗时是否不可接受而不是看执行计划里有没有“filesort”这四个字母。7.2 误区二只调sort_buffer_size不压缩返回列很多慢查询的根源是SELECT *把大字段拖进了排序缓冲区。在排序相关查询里务必先审视SELECT清单把不需要的字段拿掉。特别是在报表接口场景客户端根本不用content字段却取出来参与排序内存和网络的浪费双倍。先瘦身再调buffer顺序不能反。7.3 误区三忽略了排序的稳定性与字符集排序规则这个问题面试很少考到但在业务上很容易埋雷不同字符集和排序规则下字符串排序的字节长度不同。比如utf8mb4下一个中文字符占4字节排序键如果是VARCHAR(50)的中文昵称按最大长度算就可能超过max_length_for_sort_data触发rowid排序。这类隐形超阈值有时候比显式的大字段更难排查。7.4 误区四翻页越多rowid排序看起来就越“不划算”有人会认为深翻页场景干脆调大sort buffer走全字段排序减少回表这其实是伪优化。深翻页的瓶颈在于排序总量而不是那几次回表。更合理的优化是改掉业务侧的翻页方式——用“上次最大ID”代替OFFSET或者在业务允许时限制最大翻页深度。从源头减少排序量比纠结排序策略有效得多。8. 我对这道面试题的一点个人体会这道题我在不同场合讲过很多次每次重新梳理都会发现新的细节。它表面上问的是“全字段排序和rowid排序”实际上问的是你对数据库执行链路的理解深度一个ORDER BY从优化器到执行器、从内存到磁盘、从索引到回表中间任何一环没打通回答就会显得虚。如果让我给准备面试的人一个具体建议那就是别把这两个词当成孤立概念背而是把“排序的完整生命周期”当成一条故事线去理解来了一个SQL → 优化器看能否走索引有序性 → 不能就走filesort → 根据元组长度和数据量选择全字段还是rowid → 排序中考虑sort buffer和merge passes → 排完通过主键回表 → 返回结果。这个故事线里的每个环节都可以往深挖一层也是一线排查慢查询时的标准思路。把这个链条安装在脑子里面试答这道题时你会有“这件事我真的做过”的底气而不是背了一道题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑