导读:本期聚焦于盲改大师创作的《MongoDB聚合管道中$first操作符如何获取分组内第一个文档字段值?》,敬请观看详情。$first是MongoDB聚合管道里的累加器操作符,通常在$group阶段配合$sort一起使用,用来取出分组内第一个文档中的指定字段值。如果只写$first而不做排序,返回的是自然存储顺序中的第一条记录,结果往往不符合业务预期。本文会从$first的语法、运行机制、与$sort的配合方式讲起,再对比$first和$last的差异,结合订单、日志和排行榜等实际场景给出可以直接运行的聚合脚本。读完你就能理解为什么有时拿到的不是最早那笔数据,以及如何稳定获取每个分组里真正的第一条记录。

MongoDB聚合管道中的$first操作符用于从一组文档中提取第一个文档的某个字段值。它并不是一个独立的管道阶段,而是作为累加器表达式出现在$group阶段里,通常配合$sort阶段先把数据排好序,再按分组键聚合。很多人在使用时容易忽略排序步骤,导致取出的第一条记录不是时间最早或数值最小的那一条。接下来我们从语法细节、排序配合、误区和实战场景几个角度逐步拆解。

MongoDB聚合管道中$first操作符如何获取分组内第一个文档字段值?

$first操作符的基本语法与运行机制

$first的典型写法是在$group阶段中指定某个字段,例如{ $first: "$createdAt" }。它表示针对当前分组内的所有文档,返回第一个文档里createdAt字段的值。这里的分组由_id决定,如果_id设置为某个业务字段,比如用户ID,那么每个用户会形成一个分组,$first就会从该用户的所有文档中取第一条。

需要特别注意,$first本身不会主动排序。它依赖文档进入$group阶段时的顺序。如果前面没有$sort,MongoDB会按照存储引擎的自然顺序读取文档,这个顺序可能是插入顺序,也可能与索引扫描顺序有关,并不能保证稳定。因此,想要稳定获取每组最早的一条记录,必须在$group之前添加$sort阶段,对时间字段做升序排序。

从MongoDB 5.0开始,$first还可以用在$setWindowFields阶段中作为窗口函数使用,但日常开发里最广泛的应用仍然是$group内部的累加器场景。在$group中,$first只能引用字段路径,不能直接写常量表达式。如果需要返回整个文档对象,可以配合$$ROOT变量,但通常建议只提取需要的字段,减少聚合结果的内存占用。

$first与$sort配合实现按条件取首条记录

假设有一个订单集合,每个订单包含userIdamountcreatedAt字段。如果要查询每个用户最早的一笔订单金额,直接按用户分组并取$first金额是不稳定的。正确做法是先按createdAt升序,再按userId分组,最后使用$first获取金额。

db.orders.aggregate([
  {
    $sort: { createdAt: 1 }
  },
  {
    $group: {
      _id: "$userId",
      firstAmount: { $first: "$amount" },
      firstCreatedAt: { $first: "$createdAt" }
    }
  }
])

这段脚本先把整个订单集合按创建时间从早到晚排序,然后按用户ID分组。因为排序后的文档顺序已经固定,$first取到的就是每个用户最早的订单金额和创建时间。如果还需要展示订单号,可以把$first: "$orderId"加进去。$sort阶段可以跟多个排序字段,例如先按用户ID、再按时间,但这会影响后续分组顺序,一般排序字段要覆盖实际需求。

当数据量很大时,$sort会消耗较多内存。MongoDB默认允许聚合管道使用100MB内存,如果排序数据超过这个限制会报错。此时可以为排序字段创建索引,让$sort利用索引顺序输出文档,避免内存排序。例如对createdAt建立索引后,$sort升序可以直接走索引扫描,性能会明显提升。如果没有索引,也可以设置{ allowDiskUse: true }允许聚合操作写入临时磁盘文件,但速度会慢一些。

另一个常见做法是把$sort放在$group之前,而不是之后。虽然两者都能写出正确结果,但先排序再分组会处理整个集合的排序,而后排序只是对分组结果排序,两者语义不同。要取每组第一条记录,必须在分组前排序,这一点不能颠倒。

$first与$last的对比及常见误区

$first和$last是一对对称的累加器。$first取分组内第一个文档的字段值,$last取最后一个文档的字段值。它们的行为同样依赖输入顺序。如果先按时间升序排序,$first是最早记录,$last是最晚记录;如果按时间降序排序,$first就成了最晚记录,$last是最早记录。很多人在不同排序方向下混淆这两个操作符的含义,导致查询结果与预期相反。

一个典型误区是认为$first总是代表插入顺序中的第一条,或者认为$group内部会自动排序。实际上MongoDB的聚合管道是流式处理,$group会接收上游阶段输出的文档,并按照文档到达的顺序累加。$sort阶段可以改变这个顺序,$match、$project等阶段不会改变顺序。如果省略$sort,不应该假设任何固定顺序。

另一个容易混淆的是$first和$top、$topN等操作符的区别。$first只取出一个字段值,$top则可以返回排序后的整个文档,并且不要求前面使用$sort。例如MongoDB 5.2及以上版本可以使用{ $top: { sort: { score: -1 }, output: "$name" } }在$group中直接指定组内排序规则。对于需要取每组前N条的场景,$topN比$first加$sort更直观,但在版本兼容性上不如$first广泛。

还需要注意,如果分组内某些文档缺少目标字段,$first会返回null。这与其他累加器如$max、$min的处理有所不同,$max会忽略null和缺失字段。因此,如果业务上希望跳过空值,可能需要在$group之前用$match过滤,或者改用$top操作符指定排序规则并处理缺失值。

实际应用场景:排行榜、最新状态与去重

排行榜是$first的经典应用之一。比如游戏玩家每局得分记录在一个集合中,每个玩家可能有多条记录。要找出每个玩家的最高分,可以先按得分降序排序,再按玩家ID分组,用$first取出最高分和对应时间。这样每个玩家只返回一条记录,便于前端展示排行榜。

db.game_scores.aggregate([
  {
    $sort: { score: -1, playedAt: -1 }
  },
  {
    $group: {
      _id: "$playerId",
      highestScore: { $first: "$score" },
      achievedAt: { $first: "$playedAt" }
    }
  },
  {
    $sort: { highestScore: -1 }
  }
])

这段脚本先按得分降序、再按时间降序排序,分组后$first拿到每个玩家的最高分及最近取得时间,最后再按最高分降序整理整个排行榜结果。如果玩家有多条相同最高分记录,由于第二排序字段是时间降序,$first会取到最近一次达到该分数的记录,符合多数排行榜的业务规则。

在物联网或状态监控系统中,设备会不断上报状态,集合中同一设备可能存在大量历史状态。想要获得每台设备的最新状态,可以先按上报时间降序,再按设备ID分组,用$first取状态字段和最后一次上报时间。相比先按时间升序再取$last,直接降序取$first在阅读上更符合“最新”语义。

db.device_status.aggregate([
  {
    $sort: { reportedAt: -1 }
  },
  {
    $group: {
      _id: "$deviceId",
      latestStatus: { $first: "$status" },
      lastReportedAt: { $first: "$reportedAt" }
    }
  }
])

该场景下如果设备上报频率很高,集合数据量会迅速增长。为reportedAtdeviceId建立复合索引可以显著提升排序和分组效率。同时建议定期归档历史数据,避免单次聚合扫描过多文档。

去重场景也可以借助$first实现。比如某张日志表中每条URL被访问多次,要得到每个URL最早一次访问的用户和访问时间,可以先按访问时间升序,再按URL分组取$first。这样得到的结果相当于按URL去重并保留首次访问信息。如果查询条件是最近24小时,可以在管道最前面加$match过滤时间范围,减少后续阶段处理的数据量。

$first虽然简单,但只有正确结合$sort、索引和业务规则,才能稳定拿到预期中的“第一个”字段值。在实际编写聚合管道时,先明确分组键、排序字段和排序方向,再决定使用$first还是$last,可以避免大多数数据错位问题。

MongoDB聚合管道$first操作符分组排序修改时间:2026-08-30 08:57:03

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。