在关系型数据库里,ORDER BY 是最容易引发性能问题的子句之一。当查询需要按照某些列排序而又无法利用索引的有序性时,数据库只能自己动手做排序,这个过程可能发生在内存,也可能因为数据量超过阈值而落到磁盘,性能差距可达几十倍。要真正提升排序速度,不能只靠加索引,还要理解优化器是怎么决定走索引还是走文件排序的。

为什么 ORDER BY 会成为性能瓶颈
数据库执行 ORDER BY 时,如果所选索引的顺序和排序要求不一致,就会进入排序阶段。以 MySQL 为例,这种操作叫 filesort,虽然名字带 file,但优先使用内存排序缓冲区 sort_buffer_size,不够才写临时文件。排序本身要比较行、移动数据,CPU 和 IO 开销都不小。
更隐蔽的问题是回表。如果排序字段有索引,但 SELECT 的列不在索引里,数据库要先按索引排序,再拿着主键去聚簇索引取数据,这叫“回表后排序”或“排序后回表”,取决于执行计划。一旦数据量大,随机 IO 会急剧上升。因此,判断是否慢不能只看有没有索引,而要看执行计划里是否出现 Using filesort、Using temporary。
利用联合索引避免文件排序
最直观的优化是建立与 ORDER BY 完全匹配的联合索引。比如经常按 city 升序、age 降序查询,就建 (city, age) 的索引,并注意 MySQL 8.0 之后支持指定排序方向。这样索引本身有序,优化器可以直接按索引顺序读,跳过排序阶段。
看下面这个例子,我们先建一张用户表:
CREATE TABLE user_info ( id INT PRIMARY KEY, city VARCHAR(50), age INT, name VARCHAR(50), KEY idx_city_age (city, age) ); -- 可以命中索引排序,不需要 filesort SELECT id, city, age FROM user_info WHERE city = 'Beijing' ORDER BY age DESC LIMIT 100;
如果查询是 ORDER BY city ASC, age DESC,而索引是 (city ASC, age ASC),在 MySQL 5.7 及之前仍然会 filesort;MySQL 8.0 可以用 KEY idx_city_age (city, age DESC) 解决。另外,WHERE 里的等值条件字段要放在联合索引前面,范围查询后面的字段就无法用于避免排序了。
使用覆盖索引减少回表
即使排序能走索引,如果 SELECT 列很多,回表代价依然高。覆盖索引指的是索引包含了查询所需的所有列,这样引擎只需读索引就能返回结果。对于排序场景,把经常查的字段加进联合索引末尾,能显著提速。
例如上面的查询若还要取 name,可改为:
-- 建立更宽的覆盖索引 ALTER TABLE user_info ADD KEY idx_city_age_name (city, age, name); -- 此时 Extra 显示 Using index,没有回表 SELECT city, age, name FROM user_info WHERE city = 'Beijing' ORDER BY age DESC;
不过索引变宽会占用更多磁盘和内存,写操作也更慢,需要权衡。通常只对核心慢查询做这种宽索引,而不是所有查询都堆字段。
控制返回行数与分页优化
ORDER BY 常伴随 LIMIT,但深度分页(如 LIMIT 100000, 20)依然要先排序出前十万行再丢弃,非常浪费。可用游标分页替代:记住上一页最后一条的排序值,用 WHERE 条件往后查。
-- 假设按 age 降序,上一页最后 age 是 30,id 是 100 SELECT id, age, name FROM user_info WHERE age < 30 OR (age = 30 AND id < 100) ORDER BY age DESC, id DESC LIMIT 20;
这种方式避免了偏移量扫描,性能稳定。前提是排序字段要有唯一性兜底,比如加主键,防止同值漏数据。对于前端翻页,游标方式比 OFFSET 更适合大数据集。
调整数据库参数缓解排序压力
当确实无法避免 filesort 时,可以调大排序缓冲区。MySQL 的 sort_buffer_size 每个会话独享,设太大容易内存吃紧;一般会话级动态调整比全局改更安全。
-- 当前会话放大排序缓冲到 4MB SET sort_buffer_size = 4 * 1024 * 1024; -- 查看是否用了临时表 SHOW STATUS LIKE 'Sort_merge_passes';
如果 Sort_merge_passes 增长很快,说明磁盘归并多,排序缓冲区不够。另外,max_length_for_sort_data 控制行排序还是指针排序,调大可能减少回表但更耗内存。这些参数要结合监控慢慢调,不能盲目加倍。
总结对比不同方案
我们把常见手段做个对照:
| 优化方式 | 适用场景 | 主要收益 | 副作用 |
|---|---|---|---|
| 联合索引匹配排序 | 固定排序字段的查询 | 彻底免排序 | 索引维护成本 |
| 覆盖索引 | 少数列的高频查询 | 免回表 | 索引变宽 |
| 游标分页 | 深度翻页 | 避免偏移扫描 | 不支持随机跳页 |
| 调排序参数 | 临时排序不可避免 | 减磁盘归并 | 内存占用上升 |
实际项目中往往是组合使用:先靠索引消灭 filesort,再用覆盖索引减 IO,最后对特殊深翻页做游标改造。用 EXPLAIN 观察 Extra 字段,是验证优化是否生效的唯一标准。
SQLORDER_BYquery_optimization修改时间:2026-08-04 08:09:34