MySQL 里 order by 为什么会慢?从过滤、候选集到分页方式一条线看清
`order by` 变慢,常常不是排序单点出了问题,而是过滤路径不够收敛、索引顺序接不上、候选集太大,再叠上分页和回表,把成本一层层抬了起来。
很多人第一次排查 order by 慢,会下意识把问题理解成:SQL 只是多写了一句排序,为什么接口时间会差这么多。
但线上真正让人头疼的,通常不是“排序这个动作本身有多贵”,而是数据库为了拿到一份有序结果,前面已经多做了很多事。
我后来越来越少把 order by 问题拆成几个平铺的根因去背。更贴近现场的方式,往往是顺着一条具体查询往下看:
- 它先是怎么过滤的
- 过滤完手里还剩多少候选记录
- 排序能不能顺着同一条索引做完
- 如果不能,还要不要额外排序、回表、跳过大量分页数据
只要把这条链看清,order by 为什么慢,通常就不会停留在一句很空的“可能是 filesort”。
先看一条最典型的排序查询
比如后台订单列表有这样一条 SQL:
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 100000, 20;
这类查询在项目里很常见:
- 先筛状态
- 再按时间倒序
- 后台翻到很后面的页
很多人第一反应是去看:create_time 上有没有索引。
但这只是很小的一部分。真正影响这条 SQL 成本的,其实是一整条链:
status = 'PAID'能先筛掉多少数据- 排序能不能接在过滤之后直接顺着索引走
- 如果接不上,是不是要对大量候选集额外排序
limit 100000, 20又是不是逼着数据库先处理前面很多无效记录
所以我更愿意把 order by 慢理解成:为了拿到最后那 20 条有序结果,数据库前面到底绕了多大一圈。
第一步,先别急着盯排序,先看过滤是不是已经把局面拉大了
还是这条 SQL:
where status = 'PAID'
order by create_time desc
limit 100000, 20;
如果 status = 'PAID' 在这张表里占比本来就很高,比如 60%、70%,那它其实没有帮你缩小多少范围。数据库手里还是会留下很大一批候选记录。
这时候你后面再谈排序,成本自然轻不下来。
所以排查排序慢时,我通常不会先问“排序字段有没有索引”,而会先问:
进入排序阶段之前,候选集到底已经被压到多小了?
这一步特别重要,因为很多 SQL 的真正问题根本不是排序机制有多差,而是排序之前留下的数据太多。
如果 EXPLAIN 里你看到的是:
rows: 180000
而最终页面只展示 20 条,那你就要很警惕了。数据库为了给你这 20 条结果,前面可能已经拿着十几万条候选数据在做后续处理。
第二步,再看过滤和排序能不能接成同一条索引路径
这是 order by 优化里最容易被误解的一层。
很多人会说:
status上有索引create_time上也有索引- 那过滤和排序不都照顾到了吗
真实执行时,事情没这么理想。
数据库真正喜欢的是:有一条索引,能把过滤和排序连成一段连续路径。
比如这条 SQL:
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 20;
如果你只有:
idx_status(status)
idx_create_time(create_time)
那优化器往往还是得做取舍:
- 先按
status找,再额外排序 - 或者沿着
create_time的顺序找,再回头筛状态
无论哪一种,通常都不是最省的路径。
而如果有的是:
idx_status_ctime(status, create_time)
情况就完全不同了。数据库更有机会先用 status 把入口收住,再顺着 create_time 的顺序往后取。这样排序就不再是一道独立重活,而更像是在同一条索引路径上的顺手输出。
所以很多 order by 慢,表面在说排序,底层其实在说一件更具体的事:
过滤路径和排序路径没有接起来。
第三步,如果接不上,就要面对 Using filesort 背后的真实含义
很多人一看到 Using filesort 就很紧张,但又说不清它到底意味着什么。
我更习惯把它理解成一句很朴素的话:
数据库没法直接顺着现成顺序把结果吐出来,所以得自己再排一遍。
这里最重要的不是术语,而是它出现在什么上下文里。
如果执行计划像这样:
key: idx_status
rows: 180000
Extra: Using where; Using filesort
那你其实已经可以把现场复盘出来了:
- 先沿着
status找出很多记录 - 这些记录还没有天然按
create_time排好 - 所以得额外排序
- 候选集又不小,这个排序就会变得很重
这也是为什么 Using filesort 不能孤立看。它值不值得紧张,往往取决于前面那批候选集到底有多大。
候选集很小,额外排序未必有多痛;候选集很大,它就会立刻变成主成本之一。
第四步,很多人以为只剩排序了,其实深分页又把成本继续放大了
还是这条 SQL:
limit 100000, 20
这类写法的问题,不只是“我要第 100001 条到第 100020 条”。对数据库来说,更现实的含义是:
- 前面那一大段数据它不能假装不存在
- 它通常还是得先找到、排序、或者至少沿顺序跳过
- 最后才轮到你真正看到的那 20 条
所以后台列表经常会出现一种很典型的现象:
- 前几页很快
- 页码越深越慢
- 数据量一上来,某些筛选条件下更明显
这时候如果你只把问题归到 order by,其实会有点失真。因为真实成本已经变成:
- 过滤不够收敛
- 排序没法直接承接
- 深分页又逼着数据库处理大量会被跳过的数据
这三件事叠在一起,RT 很容易一下子上去。
第五步,回表和 select * 往往是最后那一层隐形加重
还有一类很常见的误判是:已经把过滤和排序想明白了,却没意识到查询字段本身还在抬成本。
比如:
select *
from orders
where status = 'PAID'
order by create_time desc
limit 20;
即使你后面补了 (status, create_time) 联合索引,这条 SQL 也未必就立刻很漂亮。因为只要是 select *,数据库往往还得:
- 先顺着索引找到候选记录
- 再回表拿完整字段
如果记录本身很宽,或者候选量比你以为的大,这一步依然可能不轻。
所以有些排序 SQL 的真实画像不是“排序慢”,而是:
- 过滤路径一般
- 排序成本不低
- 回表又把整体代价继续往上抬了一截
这也是为什么很多列表查询优化到后面,会顺手做两件事:
- 尽量少查无关字段
- 能形成覆盖索引的地方尽量形成覆盖索引
它们不一定改变排序的本质,但很经常能把整条查询压下来。
把这条现场往后收一下
如果你真在现场里盯这条 SQL,通常不用把它拆成一张标准化检查表。大多数时候,跑一遍 EXPLAIN,再把执行链按前后顺序接起来,问题就已经八九不离十了。
像前面这条订单查询,我一般只会连续确认几件事:先看 status='PAID' 到底筛掉了多少数据;再看排序是不是能接在这段过滤后面;如果接不上,就接受它要额外排序;最后再问一句,limit 100000,20 这种分页是不是又把前面成本继续放大了。
这样看下来,你会发现它不是某一个知识点答错了,而是整条路径都不够短。
再把最开始那条 SQL 还原一遍
回到这条例子:
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 100000, 20;
假设你看到的执行计划是:
key: idx_status
rows: 180000
Extra: Using where; Using filesort
那这条查询为什么慢,其实已经可以顺下来了:
status先筛了一下,但筛得不够狠,候选集还是很大- 排序没法直接顺着索引完成,所以出现
Using filesort - 深分页又要求数据库处理并跳过前面大量结果
- 如果查的字段再多一点,后面还会叠回表成本
你会发现,问题从来都不只是“排序字段没有单独索引”。
真正的主线是:过滤路径、候选集规模、排序承接方式、分页方式,这几件事一起决定了 order by 的成本。
最后总结
order by 慢,很多时候不是一个孤立的排序问题,而是一条查询链在几个地方同时变重了:
- 过滤条件没有先把候选集压小
- 排序字段没法接在过滤路径后面顺着索引走
- 于是出现额外排序
- 深分页又把前面的成本继续放大
- 回表和取字段方式再补上一刀
所以我现在看这类 SQL,最关心的不是“排序慢的几个原因”这种平铺答案,而是更具体的一个问题:
为了拿到最后那一点有序结果,数据库前面到底做了多少本可以少做的事?
把这个问题想清楚,order by 的优化方向通常就会自然很多。
如果你要顺着这个问题继续查
- 想先把执行计划读明白,判断慢点落在哪:看 MySQL EXPLAIN 详解:拿到执行计划后,怎么判断 SQL 真慢在哪
- 更怀疑是索引路径本身没接起来:看 MySQL 索引为什么没按预期生效:从几条真实退化路径看起
- 还没把整条慢 SQL 的主因坐实:看 MySQL 慢查询怎么定位:从执行计划到真实瓶颈