线上订单查询接口,需要按状态筛选并按创建时间排序。随着数据量增长,查询性能明显下降。在 MySQL 中,使用 EXPLAIN 分析发现,虽然 type 为 ref(表明使用了索引),但 Extra 列却出现了 Using filesort。
-- 订单表大约 450 万行
SELECT * FROM t_order
WHERE status = 1
AND channel = 3
ORDER BY created_at DESC
LIMIT 20;
表上建了一个联合索引 idx_status_channel_created,字段是 (status, channel, created_at)。WHERE 条件用了 status 和 channel,ORDER BY 用了 created_at,三个字段全在索引里,顺序也对。
按理说 MySQL 应该直接沿着索引的有序性输出结果,根本不需要额外排序。但 explain 的结果还是出现了 Using filesort。
Using filesort 到底在干什么
别看到 filesort 就觉得 MySQL 在往磁盘写临时文件做排序,其实不一定。filesort 这个名字起得有误导性,它的含义是 MySQL 无法利用索引的有序性来完成排序,不得不自己在内存里(或磁盘上)做一次额外的排序操作。
至于用不用磁盘,取决于 sort_buffer_size 够不够装下要排序的数据。装得下就在内存里排,装不下才会用磁盘临时文件。但不管在哪排,只要出现了 filesort,就意味着多了一次排序操作,这在数据量大的时候就比较耗时了。
联合索引的有序性是怎么工作的?
要理解为什么会出现 filesort,得先搞明白联合索引的排序规则。
假设联合索引是 (a, b, c),在 B+ 树里,数据的排列顺序是这样的:
索引 (a, b, c) 在 B+ 树叶子节点上的排列:
(1, 1, 10)
(1, 1, 20)
(1, 2, 5)
(1, 2, 15)
(2, 1, 3)
(2, 1, 8)
(2, 3, 1)
(2, 3, 12)
规则很简单:先按 a 排,a 相同按 b 排,b 相同按 c 排。 这就是所谓的最左前缀有序性。
注意两个关键细节:
- 当
a = 1 时,b 在这些记录内是有序的(1, 1, 2, 2)
- 当
a = 1 AND b = 1 时,c 在这些记录内是有序的(10, 20)
但如果 a = 1 而 b 不固定,c 是什么顺序?看看上面的数据:b=1 时 c 是 10、20,b=2 时 c 跳到了 5、15。c 在全局范围内并不有序,它只在 a 和 b 都确定的情况下才有序。
这就是问题的根源。
根本原因:范围查询打断了索引的有序传递
回到最初的例子,索引是 (status, channel, created_at),查询条件是 WHERE status = 1 AND channel = 3 ORDER BY created_at DESC。
status 等值、channel 等值,两个字段都用了 = 匹配。在这种条件下,created_at 在匹配到的记录范围内是有序的。MySQL 可以直接沿索引顺序读取,不需要 filesort。
但问题来了——线上的实际查询并不总是这么干净,同事后来改了查询逻辑:
SELECT * FROM t_order
WHERE status = 1
AND channel > 2
ORDER BY created_at DESC
LIMIT 20;
把 channel = 3 改成了 channel > 2,就这一个字符的差别,Using filesort 就出来了。
因为 channel > 2 是范围查询,MySQL 用索引定位到 status = 1 AND channel > 2 的记录后,这些记录里 channel 的值可能是 3、4、5、7……在每个 channel 值内部,created_at 确实是有序的,但跨不同 channel 值的 created_at 就不是有序的了。
用具体数据来看:
满足 status=1 AND channel>2 的索引记录:
(1, 3, 2026-07-01 08:00:00) -- channel=3 内部 created_at 有序
(1, 3, 2026-07-05 10:30:00)
(1, 3, 2026-07-10 14:00:00)
(1, 5, 2026-07-02 09:00:00) -- 到 channel=5,created_at 跳回去了
(1, 5, 2026-07-08 16:00:00)
(1, 7, 2026-07-03 11:00:00) -- 到 channel=7,又跳回去了
(1, 7, 2026-07-15 20:00:00)
如果要按 created_at DESC 排序,正确的顺序应该是 7 月 15 号、7 月 10 号、7 月 8 号……但索引里的物理顺序并不是这样的。MySQL 没办法直接按索引顺序读出来就是最终排序结果,只能先把符合条件的记录都捞出来,再做一次额外的排序。这就是 Using filesort 的来源。
简单总结一下这个规则:联合索引中,范围查询(>、<、BETWEEN、!=)会打断后续字段的有序性传递,范围条件之后的字段,无法再利用索引排序。
ORDER BY 的字段顺序和索引不一致
即使所有 WHERE 条件都是等值查询,ORDER BY 的字段顺序如果和索引定义的顺序对不上,一样会触发 filesort。
-- 索引 (status, channel, created_at)
-- 这条 SQL 会出现 filesort
SELECT * FROM t_order
WHERE status = 1
ORDER BY created_at, channel;
索引的排列顺序是先 channel 后 created_at,但 ORDER BY 写的是先 created_at 后 channel,顺序反了,MySQL 没法利用索引的有序性,只能自己排。
这个其实不难理解,索引 (status, channel, created_at) 在 status = 1 的条件下,数据是先按 channel 排再按 created_at 排的,你让它先按 created_at 排,它做不到。
ASC 和 DESC 混用
-- 索引 (status, channel, created_at)
SELECT * FROM t_order
WHERE status = 1
AND channel = 3
ORDER BY channel ASC, created_at DESC;
一个升序一个降序,在 MySQL 8.0 之前,B+ 树索引只支持单一排序方向,要么全升序,要么全降序(反向扫描)。当你 ORDER BY 里一个字段要 ASC 另一个要 DESC,索引没法同时满足两个方向,只能 filesort。
MySQL 8.0 引入了降序索引,可以在建索引的时候指定每个字段的排序方向:
-- MySQL 8.0+ 支持
CREATE INDEX idx_status_channel_created
ON t_order (status ASC, channel ASC, created_at DESC);
这样建出来的索引,在 status = 1 AND channel = 3 ORDER BY channel ASC, created_at DESC 的场景下就能直接利用索引排序了。
但如果你的 MySQL 版本还在 5.7,那就没办法了,8.0 之前的 DESC 关键字在建索引时会被直接忽略。
验证和排查
遇到怀疑索引排序没生效的情况,最直接的方式就是用 EXPLAIN 看 Extra 列:
EXPLAIN SELECT * FROM t_order
WHERE status = 1
AND channel > 2
ORDER BY created_at DESC
LIMIT 20;
id: 1
select_type: SIMPLE
type: range
key: idx_status_channel_created
rows: 12890
Extra: Using index condition; Using filesort
看到 Using filesort 就说明排序没走索引。如果只有 Using index condition 或者 Using where 而没有 Using filesort,说明排序是靠索引完成的。
如果确认是范围查询导致的,有几种调整思路:
方案一:调整索引字段顺序,把排序字段提前
如果业务上 channel 字段的筛选不太严格,区分度低,可以考虑把 created_at 放到范围字段前面:
-- 新索引:把 created_at 提到 channel 前面
ALTER TABLE t_order ADD INDEX idx_status_created_channel (status, created_at, channel);
这样 WHERE status = 1 ORDER BY created_at DESC 可以直接走索引排序。但代价是 channel 的过滤不能走索引了,需要在回表后过滤。适不适合取决于 channel 条件能过滤掉多少数据,过滤比例高的话反而不划算。
方案二:把范围条件转成等值条件
如果 channel 的取值范围是有限的,可以用 IN 替代范围查询:
-- channel > 2 且 channel 只有 3, 5, 7 这几个值
SELECT * FROM t_order
WHERE status = 1
AND channel IN (3, 5, 7)
ORDER BY created_at DESC
LIMIT 20;
MySQL 的优化器中,IN 列表中的每个值都被当作等值条件处理,对于 channel IN (3, 5, 7),MySQL 会分别在 channel=3、channel=5、channel=7 三个范围内利用索引的有序性,然后做一次多路归并排序,这比全量 filesort 高效得多,explain 里也不会出现 Using filesort。
但这个方案有个前提,IN 列表不能太长,如果 channel 有上百个取值,IN 列表拉得太长,优化器可能会放弃索引改走全表扫描。
方案三:利用覆盖索引减少 filesort 的代价
如果 filesort 实在避免不了,可以通过覆盖索引让排序的数据量变小:
-- 先在索引内完成筛选和排序,只拿主键 id
SELECT * FROM t_order
WHERE id IN (
SELECT id FROM t_order
WHERE status = 1
AND channel > 2
ORDER BY created_at DESC
LIMIT 20
);
子查询里只访问索引列,filesort 操作的数据量会小很多,不需要回表拿完整行数据再排序,外层再用主键 IN 回表取完整数据,在大数据量场景下这个差距非常明显。
说在最后
联合索引的排序能力不是看字段在不在索引里,而是看字段在索引里的有序性有没有被前面的查询条件破坏。