MongoDB 6.0 前聚合排序慢因 $sort 默认不走索引下推,需全量加载内存排序;6.0 起仅当 $sort 紧接 $match 后、字段为复合索引前缀、$match 条件覆盖索引前缀时才可下推。

聚合排序为什么在 MongoDB 6.0 之前特别慢?
因为 $sort 阶段默认不走索引下推,数据先全量拉到内存(或临时磁盘)再排序。尤其当 $match 后仍剩大量文档、且排序字段无合适复合索引时,executionStats 里会看到 totalDocsExamined 远大于 nReturned,sortStage 显示 memUsage 飙升甚至触发 Sort exceeded memory limit 错误。
MongoDB 6.0 引入「聚合管道索引下推(Pipeline Index Pushdown)」机制,但仅对满足特定结构的 pipeline 生效——不是所有 $sort 都能自动加速。
哪些聚合写法能触发索引下推?
必须同时满足以下条件,MongoDB 才可能把 $sort 下推到存储层(即用索引物理顺序直接返回结果),跳过内存排序:
-
$sort是 pipeline 中第一个或紧接在$match之后的 stage(不能有$project、$addFields等中间变换) -
$sort字段是索引前缀(例如索引为{status: 1, createdAt: -1, userId: 1},则{status: 1}或{status: 1, createdAt: -1}可下推,但{createdAt: -1}不行) -
$match条件字段需构成该索引的前缀(例如索引{a: 1, b: 1, c: -1},$match: {a: 1, b: 2}可下推;若只$match: {b: 2},则无法利用索引定位,自然也无法下推$sort) - 没有启用
allowDiskUse: true的显式声明(下推本身不依赖该选项,但加了反而可能干扰优化器判断)
验证是否生效:执行带 .explain("executionStats") 的聚合,查看 executionStages 中是否有 IXSCAN 直接输出已排序数据,且 sortStage 消失或变为 sortStage: { type: "sort", works: 0 }。
如何设计索引让 $sort 真正下推?
关键不是“给排序字段建索引”,而是按 $match + $sort 的联合访问模式建**复合索引**,顺序严格遵循「等值匹配字段 → 范围匹配字段 → 排序字段」:
- 等值字段(
$match中field: value)放最前,如{status: "active"} - 范围字段(
$match中field: {$gt: ...})其次,如{createdAt: {$gte: ISODate("...")}} - 排序字段放最后,方向必须与
$sort一致,如$sort: {score: -1}→ 索引中对应字段也用-1
示例:聚合为
db.orders.aggregate([
{ $match: { status: "shipped", placedAt: { $gte: ISODate("2023-01-01") } } },
{ $sort: { total: -1 } }
])
对应最优索引是 {status: 1, placedAt: 1, total: -1}。如果把 total 放前面,或方向不一致,下推就会失败。
容易被忽略的坑:$limit 和 $skip 对下推的影响
$limit 本身不影响下推,但若出现在 $sort 之前(比如想先取 top-N 再排序),整个逻辑就错了——$limit 会截断未排序数据,结果不可靠。正确顺序永远是 $match → $sort → $limit。
$skip 是重灾区:只要 pipeline 中出现 $skip,无论位置前后,MongoDB 6.0 默认禁用索引下推(因跳过行为破坏索引物理顺序的可预测性)。如果必须分页,改用「锚点分页(seek-based pagination)」:$match 加上上次结果的排序键作为边界条件,例如 {total: {$lt: 987}} 替代 $skip: 100。
另外,$sort 字段存在大量重复值时,即使下推成功,索引扫描仍可能回表读取完整文档做二次比较,此时 executionStats.totalKeysExamined 会明显高于 nReturned——这是正常现象,不代表下推失效,但提示你可能需要把排序字段选择性提高(比如加一个低基数字段组合去打破重复)。











