导读:本期聚焦于苏锦程创作的《MongoDB聚合管道中如何使用$sum实现分组求和统计?》,敬请观看详情。$sum是MongoDB聚合管道中最常用的累加操作符之一,配合$group阶段可以快速完成分组求和统计。本文将从聚合管道的基本原理讲起,详细讲解$sum在$group和$project两个阶段中的不同用法,包括单字段求和、多字段组合统计、条件求和以及处理null值时的常见陷阱。文中还给出电商订单销售额统计、按天汇总销量等典型场景的完整示例,并对比$sum与$avg、$count等操作符的区别,帮助你写出正确高效的聚合查询语句,避免分组后数据丢失或统计结果偏小的隐蔽问题。

做数据统计的时候,单靠MongoDB的普通查询语句往往力不从心。比如要统计每个用户的订单总金额、每个月的销售额、每类商品的销售数量,这类需求都需要先分组再汇总,而聚合管道正是为此而生的工具。在众多累加类操作符里,$sum无疑是使用频率最高的一个,它可以在分组时对指定字段累加求和,也能在投影阶段快速统计文档或数组元素的数量。这篇文章围绕$sum的典型用法展开,配合实际场景把语法细节和常见坑都梳理清楚。

MongoDB聚合管道中如何使用$sum实现分组求和统计?

$sum的基本语法与工作原理

$sum主要出现在两个位置:一个是$group阶段,用于对分组内的文档字段做累加;另一个是$project、$addFields等投影类阶段,用于对数组元素求和或统计数量。两种位置的语法略有差异,先看$group中的写法:

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

上面的语句把订单集合按userId分组,totalAmount对每条记录的amount字段累加,orderCount则通过累加常量1来统计每个分组内的文档数量。这里有个值得注意的点:{ $sum: 1 }是统计文档条数的惯用写法,等价于统计文档数量。如果字段引用写的是"$amount",MongoDB会逐条取值累加;遇到缺失字段或null时会被跳过,不会报错,但也不会计入结果。

在$project或$addFields阶段,$sum的参数是一个表达式或数组。直接对数组使用时,可以对数组内的数值元素求和:

db.products.aggregate([
  {
    $project: {
      name: 1,
      totalScore: { $sum: [ "$score1", "$score2", "$score3" ] }
    }
  }
])

这种写法会把三个字段的值加在一起。需要注意的是,如果数组中含有非数值或者null,$sum在非数值上的行为是忽略它们而不是中断执行,这一点与手写$add不同,$add遇到非数值会直接报错。正是这个特性让$sum在处理脏数据时更加宽容。

典型场景:订单销售额分组统计

用一个电商订单集合来演示更完整的统计流程。假设文档结构如下:

{
  "_id": ObjectId("..."),
  "userId": "u1001",
  "amount": 258.5,
  "status": "paid",
  "createdAt": ISODate("2024-03-15T08:30:00Z")
}

需求是统计每个用户的已支付订单总金额和订单数,并按金额倒序排列。完整的聚合管道需要串联$group、$sort两个阶段:

db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $group: {
      _id: "$userId",
      totalAmount: { $sum: "$amount" },
      orderCount: { $sum: 1 }
    }
  },
  { $sort: { totalAmount: -1 } },
  { $limit: 10 }
])

这里有一个性能要点必须强调:过滤条件的$match要放在管道最前面。因为$match如果位于$group之前,可以直接利用索引缩小扫描范围;如果放在后面,MongoDB就要先对所有文档分组,再从分组结果里过滤,数据量大时差距会非常明显。这也是聚合管道优化的第一条原则——尽早过滤,尽早缩小数据集。

再进一步,如果要在分组统计后只保留金额超过1000的用户,不能在$group里直接判断,需要追加一个$match阶段,此时过滤的对象已经是分组后的结果字段了:

db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $group: {
      _id: "$userId",
      totalAmount: { $sum: "$amount" }
    }
  },
  { $match: { totalAmount: { $gt: 1000 } } }
])

同一个操作符在不同阶段语义不同,这是聚合管道初学者最容易混淆的地方。理解数据在管道中是逐阶段流动变换的,写复杂查询时思路会清晰很多。

按日期分组汇总与条件求和

按月或按天汇总销售额是另一个高频需求。直接按createdAt分组会把每条记录都当成独立分组,必须先用$dateToString或$dateTrunc提取时间维度:

db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $group: {
      _id: {
        year: { $year: "$createdAt" },
        month: { $month: "$createdAt" }
      },
      monthlyTotal: { $sum: "$amount" }
    }
  },
  { $sort: { "_id.year": 1, "_id.month": 1 } }
])

分组键可以是复合对象,统计结果中_id会以子文档形式呈现年月信息。如果觉得嵌套结构不好处理,可以在后面追加一个$project把字段拍平。MongoDB 5.0之后还可以用$dateTrunc更简洁地按时间桶分组,写法上省去了手动提取年月。

条件求和也是常见变体:想在一次查询里同时统计已支付总额和已取消总额,可以借助$cond构造条件表达式,让满足条件的记录计入求和,不满足的加0:

db.orders.aggregate([
  {
    $group: {
      _id: "$userId",
      paidTotal: {
        $sum: {
          $cond: [
            { $eq: ["$status", "paid"] },
            "$amount",
            0
          ]
        }
      },
      cancelledTotal: {
        $sum: {
          $cond: [
            { $eq: ["$status", "cancelled"] },
            "$amount",
            0
          ]
        }
      }
    }
  }
])

这种写法把原本需要两次查询的统计合并成一次,减少了数据库往返。$cond的第三个参数写0很关键,如果写成null虽然也会被忽略,但显式给0语义更明确,便于维护者理解意图。

常见陷阱与注意事项

第一个坑是字段类型不一致。$sum只会累加数值类型的值,字符串数字比如"100"会被静默跳过,导致统计结果偏小且没有任何报错提示。遇到统计数字明显不对时,优先检查字段类型,可以用下面的管道排查:

db.orders.aggregate([
  {
    $group: {
      _id: { type: { $type: "$amount" } },
      count: { $sum: 1 }
    }
  }
])

第二个坑是浮点精度。金额字段如果存的是double类型,累加后可能出现类似258.49999999999997的结果。对精度敏感的业务建议存分为单位的整数,或者使用Decimal128类型,聚合时配合$toDecimal转换。

第三个坑是分组后内存限制。默认情况下单个聚合阶段占用内存不能超过100MB,分组键基数特别大时容易触发报错。解决办法要么给查询字段建索引并利用索引加速,要么谨慎使用allowDiskPolicy选项让阶段溢出到磁盘,但磁盘溢出性能会明显下降,根源上还是要控制分组数量和前置过滤数据量。

最后区分一下$sum和几个相近操作符:$avg求平均值,$count统计文档数,$sum既能求和也能通过累加1统计数量。选择哪个取决于业务语义,统计金额用$sum,算客单价用$avg配$sum组合,单纯数记录数用$count更直观。把这些操作符搭配熟练,绝大多数统计报表需求都能用一条聚合管道搞定。

MongoDB聚合管道$sum分组统计修改时间:2026-09-15 22:52:40

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