MongoDB的聚合管道功能强大,但在分组统计的场景里,经常会有一个让人头疼的需求:分组之后,不仅要拿到分组的汇总值,还想取出每个分组内的前N条记录。比如电商系统里统计每个品类销量最高的前三款商品,或者社交应用里查看每个用户最近发布的几条动态。在MongoDB 5.0之前,要实现这种需求往往需要$push把整个分组塞进数组再配合$slice切割,处理不当还可能撑爆16MB的文档限制。而$firstN操作符的出现,让这类查询的写法变得清晰许多。

$firstN的基本语法与参数说明
$firstN是MongoDB 5.0版本引入的聚合数组操作符,作用是在$group阶段中返回每个分组内前N个文档的表达式结果。它的语法结构如下:
{
$group: {
_id: "$category",
topValues: { $firstN: { input: "$sales", n: 3 } }
}
}从语法可以看出,$firstN接收一个文档对象作为参数,这个文档包含两个关键字段。input指定要取值的表达式,可以是普通的字段路径,比如$sales,也可以是更复杂的组合表达式。n指定要取的元素个数,必须是一个正整数,如果传入了0或负数,聚合会直接报错。
需要注意的一点是,$firstN返回的是一个数组,数组长度最多为n,如果分组内的文档数量不足n个,返回的数组就只有实际数量的元素。比如某个分组只有2条记录而你指定n为5,返回的数组长度就是2,不会用空值补齐。
另外还有一个语法变体,允许直接传入一个对象字面量作为input,此时MongoDB会提取对象中的n字段和input字段,这种写法在处理文档数组时比较有用,本文后面会结合实例说明。
实际案例:单字段取前N个值
先准备一份测试数据,模拟一个商品销售集合sales,每条文档包含商品名、所属品类和销量三个字段。假设现在的需求是:找出每个品类中销量最高的前三款商品。由于$firstN依赖文档进入$group阶段的顺序,所以要先用$sort按销量降序排列,再进行分组。
// 插入测试数据
db.sales.insertMany([
{ name: "商品A", category: "手机", sales: 320 },
{ name: "商品B", category: "手机", sales: 580 },
{ name: "商品C", category: "手机", sales: 150 },
{ name: "商品D", category: "手机", sales: 460 },
{ name: "商品E", category: "电脑", sales: 720 },
{ name: "商品F", category: "电脑", sales: 390 },
{ name: "商品G", category: "电脑", sales: 810 }
])
// 每个品类取销量前三的商品名
db.sales.aggregate([
{ $sort: { sales: -1 } },
{
$group: {
_id: "$category",
topProducts: { $firstN: { input: "$name", n: 3 } }
}
}
])执行后,手机分组会返回销量前三的商品名数组,即商品B、商品D、商品A,电脑分组因为只有三条记录,会返回全部三个商品名。这里的关键在于$sort阶段必须写在$group之前,因为$firstN取的是文档进入分组时的顺序,不排序的话取到的就是自然顺序的前几个,结果没有任何业务意义。
如果还想同时拿到销量数值,可以在$firstN的input里使用文档字面量写法,把多个字段打包成一个对象返回:
db.sales.aggregate([
{ $sort: { sales: -1 } },
{
$group: {
_id: "$category",
topProducts: {
$firstN: {
input: { name: "$name", sales: "$sales" },
n: 3
}
}
}
}
])这种写法返回的数组每个元素都是一个包含name和sales两个字段的对象,前端展示时不需要再反查原集合,一次查询就能拿到完整数据,减少了网络往返开销。
$firstN与$topN、$push方案的对比
同样是分组取前N条的需求,MongoDB还提供了$topN和$topN操作符,很多开发者容易混淆这几者的区别。$topN的功能其实更贴合排名场景,它允许在操作符内部直接指定排序规则,不需要单独写$sort阶段:
db.sales.aggregate([
{
$group: {
_id: "$category",
topProducts: {
$topN: {
input: "$name",
n: 3,
sortBy: { sales: -1 }
}
}
}
}
])两者的核心差异在于排序的处理方式。$firstN依赖管道前序阶段的排序结果,属于先排序后取值;$topN则在分组内部自己完成排序,逻辑上更加内聚。如果聚合管道中还有其他阶段依赖同一个排序结果,用$firstN可以避免重复排序;如果只是单纯想按某个字段取前N,$topN写法更简洁,也不容易因为忘记写$sort而得出错误结果。
再看老式的$push加$slice方案。这种写法要把分组内所有文档都推进一个数组,内存占用随着分组规模线性增长,一旦分组内文档过多,很容易触发BSON文档16MB的限制导致聚合失败。而$firstN和$topN在实现上只保留前N个元素,超出部分不会累积,内存开销可控,处理大数据集时明显更安全。
使用$firstN的注意事项
第一点是排序依赖问题,这也是最容易踩的坑。前面反复强调过,$firstN严格按照文档进入$group的顺序取值,如果管道中没有$sort阶段,取到的所谓前N个其实是未定义顺序的数据。还有一种隐蔽的情况:在$group和$sort之间插入了其他可能打乱顺序的阶段,比如$lookup,也会导致排序失效。保险的做法是把$sort紧贴着放在$group之前。
第二点是n值的类型约束。n必须是正整数的常量表达式,可以是数字字面量,也可以是能求值为整数的表达式,但不能引用分组内的文档字段。如果n求值结果不是整数,比如传入了2.5,MongoDB会直接报错。实际项目中建议把n定义成变量传入,方便统一调整。
第三点是数组元素的处理细节。如果input表达式求值结果为missing,$firstN会自动跳过该值,不会在结果数组中占位。但如果求值结果为null,null会被保留在数组中。理解这个区别对清洗数据很有帮助,比如某些文档缺少销量字段时,用$ifNull把missing转换成默认值可以避免统计遗漏。
最后一点是版本兼容性。$firstN要求MongoDB 5.0及以上版本,如果项目还在使用4.x版本,只能退回到$push加$slice的组合方案,或者升级数据库版本。升级前务必确认驱动版本的兼容性,旧版驱动可能不认识新的聚合操作符,会在客户端侧就抛出解析错误。
总结
$firstN让MongoDB分组取前N条这类高频需求的实现成本大幅降低,配合$sort阶段即可完成大多数排名类统计。追求写法简洁可以用$topN替代,处理超大数据集时则要优先考虑内存安全。掌握这几个操作符的适用边界,聚合查询的编写会事半功倍。
MongoDB聚合管道$firstNMongoDB分组取前N条修改时间:2026-09-16 05:30:35