MongoDB的聚合管道提供了一整套用于数据加工与统计的 stage 与表达式,其中 $avg 是计算平均值时最常用的表达式之一。它可以在 $group 阶段中对某一字段做分组求平均,也可以在 $project 或 $addFields 阶段中对数组内的元素求平均。理解它的输入输出规则,能够避免很多统计偏差问题。

在$group阶段中使用$avg进行分组平均
最常见的用法是在 $group 中配合 _id 指定分组键,然后使用 $avg 对数值字段求平均。例如我们有一组订单数据,想要按城市计算客单价,就可以在管道中先 $group 再 $avg。这种方式把计算下推到数据库引擎,不需要把全量文档拉到应用层。
需要注意的是,$avg 会忽略非数值类型与缺失字段。如果某个文档的 amount 字段是字符串或不存在,它不会参与计算,也不会报错。这在脏数据场景下是有利的,但也可能导致样本数少于预期。建议在管道前面用 $match 过滤掉明显无效的记录。
下面是一段在 mongo shell 中使用的聚合示例,统计每个 city 的平均订单金额:
db.orders.aggregate([
{ $match: { amount: { $type: "number" } } },
{
$group: {
_id: "$city",
avgAmount: { $avg: "$amount" },
count: { $sum: 1 }
}
},
{ $sort: { avgAmount: -1 } }
]);
在$project中对数组元素求平均
除了对文档字段求平均,$avg 也能接收一个数组表达式,并计算数组中所有数值元素的平均值。这个能力在评价系统中非常实用,比如一个用户文档里包含多个月评分的数组,我们想直接算出平均评分,而不必先 $unwind。
当数组为空或者数组中没有任何数值元素时,$avg 的结果是 null,而不是 0。这一点和业务直觉不同,很多开发者会误以为没有数据就是零分。如果你的前端需要展示为零,必须在管道后使用 $ifNull 做兜底处理。
以下示例在 $project 中直接计算每个用户 scores 数组的均值:
db.users.aggregate([
{
$project: {
name: 1,
avgScore: {
$avg: "$scores"
},
safeAvg: {
$ifNull: [ { $avg: "$scores" }, 0 ]
}
}
}
]);
性能与准确性注意事项
在使用 $avg 构建报表查询时,索引的利用情况直接决定响应速度。如果管道以 $match 开头且过滤字段有索引,MongoDB 可以大幅减少进入聚合的文档数量。而 $group 阶段本身通常会触发内存中的哈希聚合,当数据量超过 100MB 且无外部排序支撑时可能报错,此时应考虑开启 allowDiskUse。
准确性方面,混合类型数据是最大隐患。比如金额字段在前期写入时既有数字也有字符串,$avg 静默丢弃字符串会导致平均值偏高或偏低。可以在 $group 前增加 $match 与 $addFields 做类型清洗,或者利用 $convert 统一转换。
下面的例子演示了先清洗类型再求平均的稳健写法:
db.records.aggregate([
{
$addFields: {
cleanVal: { $convert: { input: "$val", to: "double", onError: null } }
}
},
{ $match: { cleanVal: { $ne: null } } },
{
$group: {
_id: "$type",
avgVal: { $avg: "$cleanVal" }
}
}
], { allowDiskUse: true });
通过上述方式,我们可以在复杂业务下既保证 $avg 的计算效率,也避免由于类型混乱带来的统计错误。合理搭配 $match、$convert 与 $ifNull,能让聚合管道更加健壮。