找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4057

积分

0

好友

535

主题
发表于 2 小时前 | 查看: 3| 回复: 0

线上订单查询接口,需要按状态筛选并按创建时间排序。随着数据量增长,查询性能明显下降。在 MySQL 中,使用 EXPLAIN 分析发现,虽然 typeref(表明使用了索引),但 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 条件用了 statuschannel,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 = 1b 不固定,c 是什么顺序?看看上面的数据:b=1c 是 10、20,b=2c 跳到了 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;

索引的排列顺序是先 channelcreated_at,但 ORDER BY 写的是先 created_atchannel,顺序反了,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=3channel=5channel=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 回表取完整数据,在大数据量场景下这个差距非常明显。

说在最后

联合索引的排序能力不是看字段在不在索引里,而是看字段在索引里的有序性有没有被前面的查询条件破坏




上一篇:Jackson 3 新特性全解析:JDK17 基线、包名变更与迁移实践
下一篇:梁文锋闭门谈话:为何英伟达在掘自己的坟墓?解析CUDA依赖破局与AI持续学习
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-24 08:14 , Processed in 0.681868 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表