$first不保证返回业务意义上的“第一条”,仅取输入流中进入分组的第一条文档字段值;必须前置$sort确保顺序,且排序需覆盖分组键与业务序字段(如user_id和created_at),否则结果不可控。

为什么 $first 不总是返回你想要的“第一条”?
$first 是聚合管道中 $group 阶段的累加器,但它不保证顺序——它只取输入文档流中**进入该分组的第一条文档对应字段的值**。如果没提前 $sort,MongoDB 会按物理存储顺序(或索引扫描顺序)提供文档,这个顺序不可控,也和插入时间无关。
常见错误现象:$first: "$name" 在未排序时每次运行结果不一致,尤其在副本集或分片集群上更明显。
- 必须在
$group前加$sort,且排序字段要能唯一确定“首条”的业务含义(比如created_at升序取最早,或updated_at降序取最新) - 如果排序字段有重复值,
$first取的是这批重复文档中“最先到达$group的那个”,仍需靠前置$sort+$limit或复合排序收束 - 性能影响:
$sort是内存敏感操作,大数据集务必在排序字段上建索引,否则可能触发Sort exceeded memory limit错误
正确写法:$sort + $group + $first 组合模板
假设集合 orders 按 user_id 分组,取每个用户创建时间最早的订单的 order_id 和 amount:
[
{ $sort: { user_id: 1, created_at: 1 } },
{
$group: {
_id: "$user_id",
first_order_id: { $first: "$order_id" },
first_amount: { $first: "$amount" },
first_created_at: { $first: "$created_at" }
}
}
]
注意:$sort 必须放在 $group 之前;user_id 和 created_at 复合排序确保同用户内时间有序;$first 才真正拿到“最早一条”。
- 不要写成
{ $sort: { created_at: 1 } }单字段排序——不同用户的订单会混排,$group时已乱序 - 如果只需要“每个用户最新一条”,把
created_at: 1改成created_at: -1,再用$first即可(不用换$last) -
$first对 null 值不跳过,如果首条文档该字段为null,结果就是null;需要过滤空值得用$cond或前置$match
$first 和 $min / $max 在“取首条”场景下的区别
当目标是“取时间最早/最晚的那条完整记录”,$first 配合排序是最直接的方式;但若只关心某个字段的极值(比如最小金额),$min 更高效且无需排序。
-
$first: "$amount"返回的是“最早那条订单的amount值”,它和该订单其他字段(如order_id)属于同一条记录 -
$min: "$amount"返回的是该用户所有订单里最小的amount数值,但无法关联到对应是哪条订单的order_id - 想同时拿到“最早订单的
order_id”和“所有订单的最低amount”,两个字段可以共存于同一$group,但前者依赖排序 +$first,后者直接用$min
分页或大结果集下 $first 的隐含风险
如果聚合管道上游用了 $skip/$limit(比如实现分页),$first 作用范围会变成被截断后的子集——它取的是“当前页内第一个分组的第一条”,不是全量数据中每个分组的第一条。
- 典型陷阱:对
orders按status分组后$limit: 10,再$first,结果只覆盖前 10 条原始文档涉及的 status,漏掉其他 status - 正确做法:分组逻辑必须在分页之前完成;分页应放在
$group之后,针对分组结果做$skip/$limit - 内存限制:MongoDB 5.0+ 支持
allowDiskUse: true,但$sort+$group组合在海量分组数下仍可能因分组键过多导致性能陡降
真正难的不是写对 $first,而是确认上游数据是否已按业务规则严格排序,以及分组键是否真的覆盖了你要聚合的全部维度——这两点一错,结果就静默偏离。











