MongoDB聚合管道里$group分组统计到底怎么用?

来源:运维教程作者:南京SEO公司头衔:草根站长
导读:本期聚焦于南京SEO公司创作的《MongoDB聚合管道里$group分组统计到底怎么用?》,敬请观看详情。同一份订单数据,使用find逐条拉回客户端再做汇总,与直接在数据库端通过$group完成统计,处理时间和网络开销可能相差几个数量级。$group是MongoDB聚合管道中的核心阶段,负责按指定键对文档分组,并借助累加器计算总和、平均值、最大值、最小值等指标。本文将围绕订单、日志等常见场景,拆解$group的基础语法、_id表达式的多种用法、常用累加器的组合方式,并给出可直接运行的聚合管道示例。同时会说明多字段分组、嵌套对象分组、分组后二次过滤排序等进阶操作,以及内存限制和磁盘溢出等容易踩到的性能问题。读完可以掌握用$group完成统计类需求的方法,减少不必要的数据传输。

MongoDB 的聚合管道由多个阶段串联而成,数据从前一个阶段流向下一阶段。$group 阶段的作用是把上一个阶段输出的大量文档按指定条件归组,每一组只输出一个汇总文档。这个阶段在订单统计、日志分析、用户行为汇总等场景中几乎无法绕开。它和 find 配合客户端循环计算相比,能把汇总压力放在数据库端,减少网络传输并利用数据库的并行执行能力。

MongoDB聚合管道里$group分组统计到底怎么用?

一、$group 的基础语法和常用累加器

$group 的语法固定为 { $group: { _id: 分组键, 字段名: { 累加器: 表达式 } } }。其中 _id 是必须提供的分组依据,可以是字段路径、常量、对象或 null。当 _id 为 null 时,所有输入文档会被归为一组,适合做全局统计。累加器负责对每组内的文档执行计算,常用的包括 $sum、$avg、$max、$min、$first、$last、$push 和 $addToSet。每个累加器都要写在分组字段的值位置,字段名可以自定义,结果会出现在输出文档中。

以订单集合为例,如果每个文档包含 status 和 amount 字段,想按状态统计总金额和订单数量,可以直接引用字段路径。引用某个字段的值需要在字段名前加美元符号,例如 $amount 表示取 amount 字段。$sum 的值如果是 1,则相当于计数。下面代码按 status 分组,计算每组的订单总金额、订单数和平均金额。

db.orders.aggregate([
  {
    $group: {
      _id: "$status",
      totalAmount: { $sum: "$amount" },
      orderCount: { $sum: 1 },
      avgAmount: { $avg: "$amount" }
    }
  }
])

需要注意,$group 不会自动排序输出文档,结果顺序通常与分组键在内存哈希表中的遍历顺序有关。如果需要稳定顺序,必须再追加 $sort 阶段。此外,$group 只能对分组后的字段进行输出,原始文档中的其他字段如果没有被累加器处理,不会出现在结果中。这个限制能保证输出结构清晰,但初学者容易在查询后直接访问未聚合字段,导致结果不符合预期。

二、多字段分组与分组后过滤排序

实际统计很少只需要一个分组维度。比如既想按地区又想按月份查看销售额,单字段分组明显不够。这时可以把 _id 写成一个文档对象,对象里的每个字段都代表一个分组维度。MongoDB 不会要求分组键必须来自原始字段,还可以使用日期表达式、字符串拼接表达式等生成新的键。多字段分组后,输出文档的 _id 会包含嵌套对象,后续排序时需要写完整路径。

在分组之前通常需要先执行 $match 过滤掉无关数据,这样可以减少进入 $group 的文档数量,降低内存占用。分组之后如果只关心金额最高的几个组,可以继续用 $sort 和 $limit。比如按照商品类目和年份统计销售额,并只保留销售额前五的组合,管道可以写成下面这样。

db.sales.aggregate([
  {
    $match: {
      saleDate: { $gte: ISODate("2024-01-01") }
    }
  },
  {
    $group: {
      _id: {
        year: { $year: "$saleDate" },
        category: "$category"
      },
      totalSales: { $sum: "$amount" },
      dealCount: { $sum: 1 }
    }
  },
  {
    $sort: { "_id.year": 1, totalSales: -1 }
  },
  {
    $limit: 5
  }
])

$sort 中引用 _id 内部字段时,需要写成 "_id.year" 这样的字符串,因为输出文档的 _id 是对象。totalSales 按 -1 降序排列,结合 $limit 5 就能得到每个年份中销售额最高的五个类目。这里 $match 放在最前面,利用 saleDate 字段上的索引可以快速缩小范围。如果先分组再过滤,性能和可读性都会变差。

三、实战案例:订单统计与性能优化

以一个典型的电商订单集合为例,每个文档大致包含 customerId、region、status、amount 和 createdAt 字段。产品需求是统计某段时间内各区域每天的总销售额、平均订单金额以及独立客户数。这个需求如果写业务代码需要在内存里维护多个 Map,数据量大时很容易出现内存溢出。用聚合管道可以让数据库完成绝大部分工作。

下面管道先限制时间范围和订单状态,再按日期字符串和地区分组。日期字段用 $dateToString 转成 yyyy-MM-dd 格式,便于按天聚合。独立客户数通过 $addToSet 收集客户 ID 后,再用 $size 计算数组长度。总金额和平均金额分别用 $sum 和 $avg,最后按日期升序、金额降序取前 20 条。

db.orders.aggregate([
  {
    $match: {
      createdAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2025-07-01") },
      status: { $in: ["completed", "paid"] }
    }
  },
  {
    $group: {
      _id: {
        day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
        region: "$region"
      },
      totalAmount: { $sum: "$amount" },
      avgOrderValue: { $avg: "$amount" },
      uniqueCustomers: { $addToSet: "$customerId" }
    }
  },
  {
    $project: {
      _id: 1,
      totalAmount: 1,
      avgOrderValue: 1,
      customerCount: { $size: "$uniqueCustomers" }
    }
  },
  {
    $sort: { "_id.day": 1, totalAmount: -1 }
  },
  {
    $limit: 20
  }
])

$addToSet 会为每个分组收集不重复的 customerId,后续 $project 用 $size 计算数组长度,从而得到独立客户数。这个方法在客户 ID 不存在重复的集合中也能使用,但如果某个订单里 customerId 为 null 或缺失,$addToSet 会把 null 视作一个值加入数组,导致独立客户数可能比实际多一。此时可以在 $match 阶段加上 customerId: { $ne: null } 过滤。

性能层面,$group 会把中间结果保存在内存中,默认允许使用 100MB。如果分组键非常多,比如要按用户 ID 分组且用户量上千万,很容易触发内存限制而报错。此时可以在 aggregate 方法中设置 allowDiskUse: true,让 MongoDB 将中间数据写到磁盘临时文件,代价是执行时间变长。另一个优化点是保证分组字段类型一致,字符串 "123" 和数字 123 会被视为不同键,导致结果分裂。索引也不能直接消除 $group 的开销,因为它仍然需要扫描匹配的文档,但合理的前置 $match 可以大幅减少输入量。

$group 是 Mongo 聚合场景中使用频率最高的阶段之一,掌握 _id 表达式、累加器组合和管道顺序之后,大部分分组统计需求都能用一条聚合语句完成。多练习从真实业务指标倒推聚合管道结构,比死记硬背语法更有效。

MongoDB聚合管道$group分组统计聚合案例修改时间:2026-09-30 11:30:30

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