MongoDB的聚合管道提供了丰富的窗口与分组操作符,其中$maxN用于在分组场景下快速提取每个组内数值最大的N个文档或字段。与传统的先排序再截断思路不同,$maxN将取前N大的逻辑内建到管道阶段中,既简化了语法,也降低了脚本维护成本。理解它的参数结构和执行顺序,是写出高效聚合查询的前提。

一、$maxN的基本语法与执行原理
$maxN通常出现在$group阶段的累加器表达式中,其基本结构包含两个必须字段:n代表要保留的最大元素个数,input指向参与比较的字段或表达式。引擎在执行$group时,会针对每个分组维护一个大小为N的有界集合,当新文档的input值大于集合内最小值时,执行替换,最终保留最大的N个。这种实现避免了全量排序,在分组基数大时优势明显。
从底层看,$maxN依赖的是一种类似堆的选择算法。假如n设为3,每个分组只需维护一个最小堆,新文档进来后若比堆顶大就替换并调整堆,分组结束堆中就是最大的3个。因此时间复杂度与文档数成正比,而非平方级。需要注意的是,如果input字段在某些文档中缺失或类型为字符串与数字混合,比较会按BSON排序规则进行,可能得到非预期结果,所以前置的$match或$project清洗很重要。
下面给出一个最简单的用法示例,统计每个城市消费最高的两笔订单金额:
db.orders.aggregate([
{
$group: {
_id: "$city",
top2Amount: {
$maxN: {
n: 2,
input: "$amount"
}
}
}
}
]);
上述查询返回的top2Amount是一个数组,里面最多包含两个金额值。如果某城市只有一笔订单,数组就只有一个元素,不会补空。这种“尽力而为”的语义让下游处理更直观,不需要判断长度是否达标。
二、与sort加slice方案的对比及适用场景
在$maxN出现之前,常见写法是先$sort再$group配合$push与$slice来取前N。这种做法逻辑上没错,但$sort阶段往往要跨越所有文档做全局排序,内存消耗大,且若数据量超过内存限制还需开启磁盘临时文件。而$maxN在分组内局部筛选,不需要全局有序,管道更容易命中索引或流式执行。
我们用一个对照示例说明旧方案:先按城市与金额降序排序,再分组取前二。代码如下:
db.orders.aggregate([
{ $sort: { city: 1, amount: -1 } },
{
$group: {
_id: "$city",
all: { $push: "$amount" }
}
},
{
$project: {
top2: { $slice: ["$all", 2] }
}
}
]);
对比可见,旧方案多出了$sort和$push全量数组两个重操作,而$maxN一步到位。不过旧方案灵活度更高,比如你想取“金额前二但去重后”的结果,可以在$push前加$group去重;$maxN本身不做去重,若业务要求唯一值需自行预处理。因此在数据量中等、逻辑简单的看板查询中优先用$maxN,复杂ETL仍可组合多阶段。
另外在分片集群上,$maxN能被下推到各个分片做本地分组,再由mongos做二次合并,网络开销小;而全局$sort则常需将大量数据拉到协调节点。对于日均千万级日志取每设备最慢五次请求的场景,这种差异会直接反映在查询延迟上。
三、结合project与unwind输出扁平结构
很多应用不希望拿到嵌套数组,而要求每行一条“组-排名-值”的宽表。这时可以在$group之后用$unwind展开$maxN的结果,再用$project加序号。虽然$unwind会增加文档数,但因为它作用在已经缩减的分组结果上,成本远小于对原始集合展开。
示例:输出每个城市金额最高的两笔,并标注名次:
db.orders.aggregate([
{
$group: {
_id: "$city",
top2: { $maxN: { n: 2, input: "$amount" } }
}
},
{ $unwind: { path: "$top2", includeArrayIndex: "rank" } },
{
$project: {
city: "$_id",
amount: "$top2",
rank: { $add: ["$rank", 1] },
_id: 0
}
}
]);
这里includeArrayIndex生成的rank从0开始,因此用$add加1得到自然名次。最终文档形如:城市A、金额900、名次1。这样的结构可直接写入分析型表或推给前端表格组件。若还需附带订单号,可将input改为文档对象,例如input: { amt: "$amount", id: "$_id" },$maxN会保留整个子文档的比较与输出,只要保证比较键在前面即可。
需要提醒的是,当input为复合对象时,排序依据的是BSON对象逐字段比较规则,若首字段相同才比后续字段。因此设计输入结构时,应把最关键的数值放在对象第一个位置,或使用$project先算出单一排序分数字段,避免歧义。掌握了这些细节,$maxN就能在排行榜、TopN异常检测、采样等场景稳定发挥作用。