在后台管理系统或社交动态流中,倒序展示最新数据并配合分页是常见需求。当用户翻到较深页码时,MySQL往往需要对大量记录排序再丢弃前面部分,这种操作在旧版本中很难被索引加速。MySQL 8.0引入了真正的降序索引,从存储层支持反向排序,为解决倒序分页性能问题提供了直接手段。

一、倒序分页的传统痛点
假设有一张消息表,包含id、user_id和create_time等字段,业务要求查询某个用户的最新动态,按create_time倒序分页。在MySQL 5.7及之前,如果只在create_time上建立升序索引,执行ORDER BY create_time DESC LIMIT 100000, 20时,优化器无法正向走索引,只能先扫描索引再反转,或者在内存或磁盘做filesort。
我们可以通过一个简化的表结构来观察。如下建表语句在旧版本中建立的索引对倒序帮助有限:
CREATE TABLE message ( id BIGINT PRIMARY KEY, user_id INT NOT NULL, create_time DATETIME NOT NULL, content VARCHAR(255), INDEX idx_user_ct (user_id, create_time) ) ENGINE=InnoDB;
当执行倒序查询时,EXPLAIN的Extra列常出现Using filesort,表示排序没有使用索引顺序。随着偏移量增大,需要排序的行数越多,响应时间呈线性恶化。这并不是代码逻辑错误,而是底层索引方向不匹配造成的结构性瓶颈。
二、MySQL 8.0降序索引的创建方式
MySQL 8.0允许在建索引时显式声明DESC,使索引叶子节点按指定方向排序。针对上述场景,可以建立混合顺序的联合索引,让user_id升序、create_time降序,从而完美贴合查询语句。
修改后的索引定义如下,注意语法中列后的DESC关键字:
CREATE TABLE message ( id BIGINT PRIMARY KEY, user_id INT NOT NULL, create_time DATETIME NOT NULL, content VARCHAR(255), INDEX idx_user_ct_desc (user_id ASC, create_time DESC) ) ENGINE=InnoDB;
这样物理存储上,相同user_id的记录已经按create_time从新到旧排列。查询时优化器可以直接沿索引向后读取,不需要额外排序。对于深分页,虽然仍要跳过前面偏移,但避免了全量排序,代价大幅降低。
从实现原理看,InnoDB在8.0中对降序索引使用前向或后向扫描指针,执行器能识别索引方向并与ORDER BY子句匹配。这意味着在联合索引里,即使部分列升序部分列降序,也能被高效利用,而旧版本会将其视为无法匹配从而放弃索引排序。
三、执行计划对比分析
我们在相同数据量下执行同一条倒序分页SQL,对比两个版本或两种索引的执行计划。查询语句如下:
EXPLAIN SELECT id, create_time, content FROM message WHERE user_id = 1001 ORDER BY create_time DESC LIMIT 100000, 20;
在仅有升序索引的表里,EXPLAIN输出可能类似:key显示为idx_user_ct,但Extra出现Using filesort,rows估算值很大,type为ref。这说明虽然用了索引过滤user_id,排序仍要靠额外步骤。
而在拥有idx_user_ct_desc的8.0实例中,EXPLAIN的Extra变为Using where,不再有filesort,key明确为idx_user_ct_desc,rows也因索引顺序读取而更接近实际返回量。通过SHOW STATUS或慢日志可看到排序相关计数器几乎为零。
| 对比项 | 升序索引方案 | 降序索引方案 |
|---|---|---|
| Extra信息 | Using filesort | Using where |
| 排序方式 | 内存或磁盘外部排序 | 索引顺序扫描 |
| 深分页耗时 | 随偏移量明显增长 | 增长平缓 |
| 优化器选择 | 可能错选全表 | 稳定用联合索引 |
通过对比能清楚看到,降序索引改变了执行计划的核心路径。开发者在调优时应重点观察Extra是否含filesort,以及key是否命中预期索引,而不是仅看是否用到索引。
四、联合索引中混合方向的最佳实践
实际业务常需按多个字段排序,例如先按状态升序、再按时间降序。MySQL 8.0支持在联合索引中自由组合ASC与DESC,但必须保证索引定义顺序与查询ORDER BY顺序一致,且方向对应。
示例如下,索引与查询语句方向严格匹配才能生效:
-- 建表时定义混合方向索引 CREATE INDEX idx_status_ct ON message (status ASC, create_time DESC); -- 查询语句顺序和方向需对应 SELECT id, status, create_time FROM message WHERE status = 1 ORDER BY status ASC, create_time DESC LIMIT 50000, 10;
如果查询写成ORDER BY status DESC, create_time ASC,则与索引方向相反,优化器无法利用索引排序优势。此外,若WHERE条件未覆盖索引前导列,索引可能完全用不上,因此设计时应把等值过滤列放在联合索引前面。
另一个注意点是,降序索引虽好,但写入时会增加少许维护成本,因为InnoDB需按指定方向调整页内记录。对于写多读少且极少倒序查询的场景,不必盲目建立。应结合执行计划与业务读写比权衡。
五、深分页的进一步优化思路
即便有了降序索引,LIMIT 100000, 20仍要逻辑跳过十万行。若业务允许,可采用游标分页,即记录上一页最后一条的create_time和id,用条件查询替代偏移。
改写后的查询不再使用大偏移量:
SELECT id, create_time, content FROM message WHERE user_id = 1001 AND (create_time < '2023-01-01 10:00:00' OR (create_time = '2023-01-01 10:00:00' AND id < 500000)) ORDER BY create_time DESC, id DESC LIMIT 20;
配合idx_user_ct_desc索引,这种写法每次只扫描固定量记录,性能与页码深度无关。降序索引保证了ORDER BY create_time DESC, id DESC能直接走索引,避免了偏移量带来的浪费。
总结来看,MySQL 8.0降序索引从存储和执行器层面补上了倒序排序的短板。通过对比执行计划中的Extra与key,可以直观验证其效果。再结合游标分页,倒序翻页难题便能系统性化解。