$avg仅计算简单算术平均,忽略数量权重,导致结果失真;正确方法是用$group分别累加“数量×单价”和“数量”,再以$divide求商,并通过$match过滤入库记录、$ifNull或$cond处理空值,结合索引优化性能。

为什么直接用 $avg 算不出加权平均单价
因为 $avg 只对字段值做简单算术平均,完全忽略每笔入库数量差异。比如:100件进价5元、200件进价6元,真实加权单价是 (100×5 + 200×6) / (100+200) = 5.67,而 $avg 会算成 (5 + 6) / 2 = 5.5——结果偏差不可接受。
必须手动构造分子(总金额)和分母(总数量),再用除法得出结果。
用 $group + $sum 计算加权平均单价的写法
核心思路:在 $group 阶段分别累加「数量 × 单价」和「数量」,最后用 $divide 求商。
- 假设库存流水集合叫
inventory_logs,每条记录含字段:sku(商品编码)、quantity(变动数量,正为入库)、unit_price(对应单价) - 只统计入库(
quantity > 0)的加权平均,需先$match - 聚合阶段示例:
[
{ $match: { quantity: { $gt: 0 } } },
{ $group: {
_id: "$sku",
total_amount: { $sum: { $multiply: ["$quantity", "$unit_price"] } },
total_quantity: { $sum: "$quantity" }
}
},
{ $project: {
sku: "$_id",
weighted_avg_price: { $divide: ["$total_amount", "$total_quantity"] }
}
}
]
注意:$multiply 和 $divide 都要求参数是数值型;如果 unit_price 或 quantity 是字符串或缺失,结果会是 null。
遇到 null 或空值时怎么安全计算
只要任意一个字段为 null、undefined 或非数字,$multiply 就返回 null,导致整条链路失效。
- 用
$ifNull做兜底:{ $ifNull: ["$unit_price", 0] },但 0 会扭曲结果,更稳妥的是过滤掉异常数据 - 增强
$match条件:{ quantity: { $gt: 0 }, unit_price: { $type: "number" }, unit_price: { $ne: null } } - 若允许部分字段缺失且不想丢数据,可用
$cond跳过该条记录:{ $cond: { if: { $and: [{ $gt: ["$quantity", 0]}, { $isNumber: "$unit_price"}] }, then: { $multiply: ["$quantity", "$unit_price"] }, else: 0 } }
性能与索引注意事项
这个聚合本身不走索引加速($group 是内存/磁盘扫描),但前置 $match 能大幅减少文档输入量。
- 务必在
quantity和sku上建复合索引,例如:db.inventory_logs.createIndex({ sku: 1, quantity: 1 }) - 如果只查单个商品,
$match加{ sku: "ABC123" }后,索引能直接定位范围,速度提升明显 - 千万级流水表慎用无
$match的全量$group,容易触发内存限制(allowDiskUse: true必须显式开启)
加权平均单价不是纯数学题,它绑着业务含义:哪些单据参与计价、是否含税、时间范围是否要加 $gte/$lt 过滤——这些逻辑一旦漏掉,算出来的数看着对,实际已经错得离谱。


















