数据库执行带 ORDER BY 的查询时,如果优化器无法利用索引的有序性,就会将满足条件的行收集到排序缓冲区,再进行 filesort。当结果集变大,排序缓冲区放不下时,数据库会借助磁盘临时文件完成归并排序,这个过程往往是慢查询的重要来源。索引之所以能优化排序,并不只是在 WHERE 条件上起作用。B+ 树索引的叶子节点按照索引键顺序连接,只要查询要求的排序方式与索引的物理顺序兼容,数据库就可以沿着索引直接读取已经排好的数据,省去额外的排序步骤。

一、ORDER BY 使用索引的前提条件
并不是给排序列单独建一个索引就一定能优化 ORDER BY。联合索引中,排序字段需要满足最左前缀规则。假设存在索引 idx_user_time(user_id, order_time, amount),那么 ORDER BY user_id, order_time, amount 可以直接使用索引;ORDER BY user_id, order_time 也可以;但如果写成 ORDER BY order_time 或者 ORDER BY user_id, amount,则无法完整利用索引完成排序,因为排序键跳过了索引中间的列,无法保证整体有序。
条件过滤和排序之间的关系也需要区分等值与范围。当 WHERE 条件对索引最左列使用等值判断时,该列相当于被固定,后续排序仍可依赖索引。例如 WHERE user_id = 1001 ORDER BY order_time 可以走索引排序;但如果条件变成 WHERE user_id > 1001 ORDER BY order_time,情况就不同了。范围条件扫描到的行在 order_time 上并不连续,数据库只能先取出结果再单独排序。理解这一点,对联合索引的列顺序设计非常关键。
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, order_time DATETIME NOT NULL, amount DECIMAL(10,2), status TINYINT, KEY idx_user_time (user_id, order_time, amount) ); -- 可以使用索引排序:等值条件固定 user_id SELECT * FROM orders WHERE user_id = 1001 ORDER BY order_time, amount; -- 通常无法直接使用索引排序:user_id 是范围条件 SELECT * FROM orders WHERE user_id > 1001 ORDER BY order_time, amount;
此外,排序方向也会影响索引的使用。在 MySQL 8.0 之前,索引默认按升序存储,如果查询需要 ORDER BY a ASC, b DESC 这种混合方向排序,早期版本很难直接利用索引,因为索引无法同时满足一列升序、另一列降序。MySQL 8.0 支持降序索引后,可以在创建索引时指定每列的排序方向,从而解决这类场景。不过即便不支持降序索引,全升序或全降序的排序仍然可以反向扫描索引实现,例如 ORDER BY a DESC, b DESC 可以通过索引反向扫描完成。
二、常见排序场景下的索引设计技巧
最典型的场景是等值过滤加排序。业务查询经常写成 WHERE status = 1 ORDER BY create_time DESC,此时建议建立 (status, create_time) 联合索引,而不是在 create_time 上单独建索引。因为 status 等值条件可以把排序范围缩小到特定 status 的连续区间,后续直接按 create_time 读取即可。单独在 create_time 上建索引,虽然索引本身有序,但无法同时过滤 status,数据库需要回表筛选,优化器很可能放弃索引。
分页查询中的排序也值得单独设计。常见深分页写法是 ORDER BY create_time DESC LIMIT 100000, 20,偏移量越大越慢。如果配合 (create_time, id) 联合索引,并采用游标方式记录上一页最后一条记录的 create_time 和 id,就可以使用行值比较直接定位到下一页起始位置,例如 WHERE (create_time, id) < (?, ?) ORDER BY create_time DESC, id DESC LIMIT 20。这种方式能避免数据库扫描大量无关行。
-- 深分页游标查询:避免大偏移量
SELECT id, create_time, title
FROM articles
WHERE (create_time, id) < ('2025-06-01 10:00:00', 90021)
ORDER BY create_time DESC, id DESC
LIMIT 20;
覆盖索引对排序优化也非常重要。如果 SELECT 列表中需要的列都包含在联合索引里,数据库可以从索引直接得到结果,不需要回表。例如查询 SELECT user_id, order_time FROM orders WHERE user_id = 1001 ORDER BY order_time,如果存在 (user_id, order_time) 索引,Extra 通常会出现 Using index,表示排序和取数都在索引内完成。即使必须返回额外列,也可以考虑先用索引完成排序和主键筛选,再回表取剩余字段,但需要评估回表代价。
多字段排序时,要尽量让 WHERE 条件中的等值列和 ORDER BY 字段形成连续的索引前缀。比如查询条件是 WHERE user_id = 1 AND status = 0 ORDER BY create_time DESC,可以考虑 (user_id, status, create_time)。两个等值条件都固定后,create_time 的顺序依然可以在索引中保证。如果等值条件之间混入范围条件,范围条件之后的排序字段通常就无法继续利用索引,这时可以通过覆盖索引减少回表或者调整查询条件来规避。
三、用执行计划验证排序是否真正走索引
判断 ORDER BY 是否使用索引,不能只看执行计划中的 key 列。key 列显示的是优化器选择的索引,但它可能只用于 WHERE 条件过滤,排序阶段仍然执行 filesort。关键要看 Extra 列是否出现 Using filesort。如果 Extra 中包含 Using filesort,说明查询需要对结果集进行额外排序,通常需要优化索引设计或改写 SQL。如果 Extra 显示 Using index,则代表覆盖索引生效,排序和取数都利用了索引。
EXPLAIN SELECT user_id, order_time FROM orders WHERE user_id = 1001 ORDER BY order_time LIMIT 20;
上面这条语句在存在 (user_id, order_time) 索引时,执行计划通常不会出现 Using filesort。如果把索引改成 (order_time, user_id),虽然 order_time 在索引首列,但 WHERE 条件无法高效过滤 user_id,优化器很可能会选择全表扫描或者使用索引后仍需 filesort,Extra 中出现 Using filesort 就是排序没有借助索引的证据。
除了 EXPLAIN,MySQL 的优化器跟踪也可以用来进一步确认排序优化过程,但日常排查中用好 Extra 列基本足够。还应注意避免在排序字段上使用函数或表达式,例如 ORDER BY DATE(create_time) 会让 create_time 列上的索引失效,因为数据库无法直接利用原始列的有序性。类型隐式转换、字符集不一致也可能导致索引排序失效,排查时建议检查 WHERE 和 ORDER BY 两侧的数据类型是否完全一致。
当索引设计合理但优化器仍未选择索引排序时,可能涉及行数估算偏差或排序成本过高。此时可以通过 FORCE INDEX 测试对比执行计划,但生产环境不建议长期强制索引。更好的做法是更新统计信息、调整索引列顺序,或者使用覆盖索引降低回表成本,让优化器自然地选择索引排序路径。最后,任何索引优化都应该结合真实数据量和查询频率来验证,避免为了一个低频排序查询维护过多索引,增加写入开销。