MongoDB的聚合管道功能强大,其中对数组的处理是一个高频需求。除了常用的$map、$filter之外,$reduce是一个容易被忽视但非常实用的操作符。它的作用是把一个数组归约成单个值,比如把一组数值累加求和、把一组字符串拼接成一句话、把嵌套数组展平成一层。理解$reduce的用法,能让很多原本需要在应用层写循环处理的逻辑直接下推到数据库端完成,减少网络传输和业务代码复杂度。

$reduce的基本语法与执行原理
$reduce的语法结构由三部分组成,看下面的表达式:
{ $reduce: {
input: <数组>,
initialValue: <初始值>,
in: <累积表达式>
} }
input是待处理的数组,可以是文档中的数组字段,也可以是其他表达式返回的数组结果。initialValue是归约的起点,也是数组为空时返回的默认值,这一点很重要,后面会专门讨论。in则是每一步的累积逻辑,其中可以通过$$value访问上一步累积的结果,通过$$this访问当前正在遍历的元素。
它的执行过程可以类比为JavaScript中的Array.prototype.reduce:从initialValue开始,依次取出数组中的每个元素,执行in表达式,把返回值作为下一轮的$$value,遍历结束后最后一次in表达式的结果就是$reduce的最终输出。需要注意的是,$reduce返回的是单个值,这个值可以是数值、字符串,也可以是数组或对象,取决于你怎么写in表达式。
典型使用场景与代码示例
场景一:数组元素累加求和
假设有一个订单集合,每条订单包含商品明细数组,现在要统计每笔订单的总金额。虽然嵌套数组求和可以用$sum配合$map实现,但$reduce的写法更直观,尤其是累加逻辑复杂时优势明显。
db.orders.aggregate([
{
$project: {
orderNo: 1,
totalAmount: {
$reduce: {
input: "$items",
initialValue: 0,
in: { $add: ["$$value", { $multiply: ["$$this.price", "$$this.qty"] }] }
}
}
}
}
])
在这个例子中,initialValue是0,每一轮用$$this.price乘以$$this.qty得到单个商品小计,再通过$add加到累积值上。如果商品有折扣字段,只需要在in表达式里再嵌套条件判断即可,灵活性比固定用途的操作符高得多。
场景二:字符串拼接
把标签数组拼接成一段可读文本也是常见需求,比如把["生鲜","水果","进口"]拼接成"生鲜/水果/进口":
db.products.aggregate([
{
$project: {
tagText: {
$reduce: {
input: "$tags",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$value", ""] },
"$$this",
{ $concat: ["$$value", "/", "$$this"] }
]
}
}
}
}
}
])
这里用$cond处理了分隔符问题:第一轮累积值为空字符串时不加分隔符,后续轮次才用$concat拼接斜杠。如果不做这个判断,结果开头会多出一个斜杠,这是新手最容易踩的坑之一。
场景三:二维数组展平
当文档中存在嵌套数组,需要把多层结构拍平成一层时,$reduce配合$concatArrays非常好用:
db.users.aggregate([
{
$project: {
allSkills: {
$reduce: {
input: "$skillGroups",
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
}
])
假设skillGroups是[["java","go"],["sql","redis"]],那么输出就是["java","go","sql","redis"]。注意这里不能直接用$reduce去拼接,因为$reduce本身不做数组扁平化,in表达式返回数组时会被当作普通值累积,所以必须借助$concatArrays显式合并。
场景四:求最大值与条件归约
求数组最大值虽然可以用$arrayMax,但如果要带业务条件,比如只统计状态为有效记录的最大分数,$reduce就更合适:
db.exams.aggregate([
{
$project: {
maxValidScore: {
$reduce: {
input: "$scores",
initialValue: null,
in: {
$cond: [
{ $and: [
{ $eq: ["$$this.status", "valid"] },
{ $or: [
{ $eq: ["$$value", null] },
{ $gt: ["$$this.score", "$$value"] }
]}
]},
"$$this.score",
"$$value"
]
}
}
}
}
}
])
initialValue设为null是为了区分没有任何有效记录的情况,配合$or判断保证第一轮比较不会出错。这种写法把过滤和归约合并在一次遍历中完成,比先$filter再求值的写法少一次数组遍历。
$reduce与其他操作符的选择对比
很多人会疑惑,求和有$sum,取最大有$max,什么时候才真正需要$reduce。判断标准很简单:当累积逻辑无法用单一内置操作符表达,或者前后两个元素之间存在依赖关系时,就轮到$reduce出场。
简单求和直接用$sum性能更好,因为内置操作符有专门的优化路径;涉及跨字段计算、条件判断、结果类型变化(比如数组转对象)时,$reduce几乎是唯一选择。另外$reduce和$map也可以组合使用,先用$map把数组元素转换成需要的形态,再用$reduce归约,两步拆开写比塞进一个复杂表达式里可读性更高。
常见报错与注意事项
第一个常见错误是input不是数组。$reduce要求input必须解析为数组或null,如果字段可能缺失,建议先用$ifNull兜底,例如{$ifNull: ["$items", []]},否则管道会直接抛错中断。第二个注意点是initialValue的类型要和in表达式返回值类型保持一致,比如累积数值就初始化为0,累积数组就初始化为[],类型混乱会导致后续轮次的表达式比较或拼接失败。
性能方面,$reduce在数据库端逐元素执行表达式,数组非常大时会消耗较多CPU。对于超大数组的复杂归约,可以考虑用$unwind配合分组聚合重写,或者把部分计算前置到写入阶段。此外,in表达式里访问的字段如果能在前期$project阶段裁剪掉无关字段,也能减少内存占用。
总的来说,$reduce是MongoDB聚合管道中处理数组归约问题的通用方案,掌握$$value和$$this这两个变量的含义是入门的关键,再结合$cond、$concatArrays等操作符组合使用,就能应对绝大多数数组到单值的转换需求。建议在测试环境用小数据集验证表达式逻辑后再上生产,避免边界情况导致管道报错。