密集排名指相同值获得相同名次且后续名次不跳过,如[100,95,95,90]对应[1,2,2,3];MongoDB 5.0无原生$denseRank,需用$rank+$lag+$sum组合实现,关键在于同阶段指定partitionBy与sortBy,避免跨组取值错误。

什么是密集排名,为什么不能直接用 $rank
密集排名(dense rank)要求相同值必须获得相同名次,且后续名次不跳过。比如 [100, 95, 95, 90] 的密集排名是 [1, 2, 2, 3];而 $rank 给出的是 [1, 2, 2, 4](跳过了 3)。MongoDB 5.0 的 $setWindowFields 不提供原生 $denseRank 算子,得靠组合实现。
用 $denseRank 的等效逻辑:$rank + $lag + $sum
核心思路是:先按排序字段分组打上基础 $rank,再识别当前行与前一行值是否相等;若相等,就继承前一名次,否则用当前 $rank 值。这需要两层窗口操作:
- 第一层:用
$rank计算原始排名,并用$lag取前一行的排序字段值(如"score") - 第二层:对
$rank序列做累计求和,但只在值变化时加 1 —— 即{ $sum: { $cond: [{ $ne: ["$score", "$prevScore"] }, 1, 0] } }
实际聚合阶段示例:
{
$setWindowFields: {
sortBy: { score: -1 },
output: {
rankRaw: { $rank: {} },
prevScore: { $lag: { output: "$score", by: "$score" } }
}
}
},
{
$set: {
denseRank: {
$sum: {
$cond: [
{ $ne: ["$score", "$prevScore"] },
1,
0
]
}
}
}
}
注意 sortBy 和 partitionBy 的配合陷阱
如果数据需按用户分组各自排名(如每个 userId 内按时间排密集名次),必须显式使用 partitionBy,且 sortBy 必须在同一个 $setWindowFields 阶段内声明——不能拆到多个阶段里。否则 $lag 会跨组取值,导致错误。
- ✅ 正确:
partitionBy: "$userId"和sortBy: { createdAt: -1 }同在一个$setWindowFields - ❌ 错误:先按
userId分组再单独跑$setWindowFields,$lag就失效了 - ⚠️ 兼容性:MongoDB 5.0 要求
partitionBy字段必须出现在sortBy或输出字段中,否则报错"partitionBy field must be in the sort key or output"
性能敏感点:索引与内存限制
$setWindowFields 是内存密集型操作,尤其在未建索引或数据量大时容易触发 "Sort exceeded memory limit"。它不会自动利用索引加速 sortBy,但你可以提前用 $sort 配合 allowDiskUse: true 缓解。
- 必须确保
sortBy字段有支持方向的复合索引,例如{ userId: 1, score: -1 } - 避免在
$setWindowFields中引用未索引的字符串字段做partitionBy,否则可能全表扫描 - 当文档数超 10 万且窗口跨度大时,考虑用应用层分页+游标方式替代单次全量密集排名
真正卡住的地方往往不是公式写法,而是没意识到 $lag 的行为依赖于 sortBy 的严格顺序,以及 partitionBy 实际上重置了整个窗口上下文——这点在调试时最容易被忽略。

















