$rank 不能直接用于 $group 后,因其是仅在 5.0+ 的 $setwindowfields 中支持的窗口函数,需配合 $sort 和 partitionby 使用;老版本需用 $push + $indexofarray 模拟,但有性能与并列处理限制。

为什么 $group 后不能直接用 $rank?
因为 $rank 是 window 函数,只在 5.0+ 版本的 $setWindowFields 阶段中可用,且必须配合 $sort 和分组上下文(partitionBy)使用。直接写在 $group 后面会报错:"unknown operator: $rank"。老版本(如 4.4 及之前)压根不支持窗口函数,只能靠变通方式模拟。
用 $setWindowFields 实现分组内 Top-N 排名(5.0+)
这是最干净、最符合语义的做法,但要注意三点:排序字段类型必须一致、partitionBy 字段必须有索引、整个阶段不能放在 $lookup 或 $unwind 之后(否则分组逻辑被破坏)。
例如:按 category 分组,对每个分组内按 sales 降序排前 3 名:
[
{ $match: { sales: { $exists: true, $type: "number" } } },
{ $sort: { category: 1, sales: -1 } },
{
$setWindowFields: {
partitionBy: "$category",
sortBy: { sales: -1 },
output: {
rank: { $rank: {} },
denseRank: { $denseRank: {} }
}
}
},
{ $match: { rank: { $lte: 3 } } }
]
-
$sort必须紧邻$setWindowFields前,且方向与sortBy一致,否则排名错乱 -
partitionBy字段若基数高(如用户 ID),建议在该字段上建索引:db.orders.createIndex({ category: 1 }) -
$match放在最后才能真正“裁剪”,否则$setWindowFields仍会对全量数据计算排名
兼容老版本的 fallback 方案:$push + $indexOfArray
适用于 MongoDB 4.2–4.4,核心思路是:先分组聚合出有序数组,再用 $indexOfArray 查当前文档在数组中的位置。缺点是内存占用高、无法处理并列($rank vs $denseRank),且文档数超万时易触发 maxMemoryUsageMB 限制。
示例:给每个 category 内销量最高的 3 条记录打上 rank 标签:
[
{ $match: { sales: { $type: "number" } } },
{
$group: {
_id: "$category",
topItems: {
$push: {
_id: "$_id",
sales: "$sales",
name: "$name"
}
}
}
},
{ $unwind: { path: "$topItems", includeArrayIndex: "rankIdx" } },
{
$addFields: {
rank: { $add: ["$rankIdx", 1] }
}
},
{ $project: { _id: "$topItems._id", name: "$topItems.name", sales: "$topItems.sales", rank: 1 } },
{ $match: { rank: { $lte: 3 } } }
]
- 必须先
$group再$unwind,否则includeArrayIndex不生效 -
$push默认按输入顺序追加,所以务必在$group前加$sort(如{ $sort: { category: 1, sales: -1 } }) - 如果原始集合没按
category排序,$group输出的topItems数组顺序不可控
最容易被忽略的坑:字段类型和索引缺失
真正让分组内排名慢或不准的,往往不是语法写错,而是底层数据“不干净”:
-
sales字段存成字符串("1200")而非数字,$sort会按字典序排,“999” > “10000” -
category字段存在null或空字符串,$group会把它们全归到同一个_id: null分组里 - 没在
category和sales上建复合索引:db.orders.createIndex({ category: 1, sales: -1 }),导致$sort阶段无法流式执行,全量入内存 - 在
$lookup后做排名——关联结果已打乱原始顺序,且$setWindowFields不支持跨集合分区
上线前务必跑一次 explain("executionStats"),重点看 stage 是否出现 COLLSCAN、sortStage 的 memUsage 是否超限、usedDisk 是否为 true。











