$group阶段不支持多个分组后直接合并,只能按单一_id表达式分组;所谓“合并”实为后续阶段处理,如$push+$reduce合并数组、$facet并行分组或$project汇总统计值。

$group 阶段本身不支持“多个分组后合并”这种操作——它每次只按一个 _id 表达式分组,输出的是多个独立分组文档。所谓“多个分组后的合并”,实际是后续阶段对分组结果的再处理,常见于字段拼接、数组聚合或跨维度汇总。
为什么不能直接用多个 $group 实现“合并”?
每个 $group 都会重置文档结构:前一个 $group 的输出必须包含 _id,而下一个 $group 只能基于这个新结构重新分组。连续使用 $group 不是“合并分组结果”,而是“嵌套分组”。比如先按 year 分、再按 month 分,最终得到的是年月组合粒度的数据,不是把不同年份的月度统计“合起来”。
真正需要的“合并”,往往是以下几种情况:
- 把多个分组结果(如按周、按商品)各自算出的数组,拼成一个大数组
- 把多个分组的统计值(如销售额、订单数)汇总到同一文档中
- 把不同条件下的分组结果(如“已完成”和“已取消”订单)横向并列展示
$push + $reduce 实现多分组结果的数组合并
当你要把多个分组产出的数组字段(例如每组的 items)合并成一个扁平数组,$push 搭配 $reduce 是最直接的方式。注意:这要求你先用 $group 把数据聚合成数组,再用第二层 $group 或 $reduce 合并它们。
示例:合并两个不同状态的订单列表为一个总列表
db.orders.aggregate([
{ $match: { status: { $in: ["completed", "cancelled"] } } },
{ $group: {
_id: "$status",
items: { $push: "$$ROOT" }
}
},
{ $group: {
_id: null,
allItems: { $push: "$items" }
}
},
{ $project: {
mergedList: {
$reduce: {
input: "$allItems",
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
}
])
关键点:
-
$push: "$$ROOT"保留原始文档结构 - 第二层
$group用_id: null把所有分组结果收拢为单个文档 -
$reduce+$concatArrays是合并嵌套数组的可靠组合;别用$unwind再$push,容易爆内存
$facet 实现多条件分组结果的横向并列
如果你要对比不同分组逻辑的结果(比如“本周销量” vs “上周销量”),$facet 是唯一能在一个管道里并行执行多个独立分组的方式。
示例:同时获取当前周和上一周的销售汇总
db.sales.aggregate([
{ $addFields: {
weekNum: { $week: "$date" },
year: { $year: "$date" }
}
},
{ $facet: {
"thisWeek": [
{ $match: { weekNum: { $eq: { $week: new Date() } }, year: { $eq: { $year: new Date() } } } },
{ $group: { _id: null, total: { $sum: "$amount" } } }
],
"lastWeek": [
{ $match: { weekNum: { $eq: { $subtract: [{ $week: new Date() }, 1] } }, year: { $eq: { $year: new Date() } } } },
{ $group: { _id: null, total: { $sum: "$amount" } } }
]
}
},
{ $project: {
diff: { $subtract: [{ $arrayElemAt: ["$thisWeek.total", 0] }, { $arrayElemAt: ["$lastWeek.total", 0] }] }
}
}
])
注意:
-
$facet内每个子管道完全独立,可包含自己的$match、$group等 - 输出是单文档,字段名为 facet 名(如
thisWeek),值为数组;即使只有一条结果也要用$arrayElemAt提取 - 不支持在
$facet外直接引用子管道的索引字段(如不能写thisWeek._id),必须靠$project显式提取
容易被忽略的性能与结构陷阱
聚合管道里“合并”动作最容易出问题的地方不在语法,而在数据形态和内存控制:
-
$unwind后立刻$group是常见误操作:如果某组有 10 万条子文档,$unwind会生成 10 万条中间文档,极大拖慢速度;优先考虑$push+$reduce替代 -
$group的_id表达式一旦含数组或嵌套对象,MongoDB 会自动深比较,可能意外触发全量扫描;尽量用标量字段(String、Number、ObjectId)做分组键 - 用
$merge或$out写回集合时,$group的输出结构必须与目标集合兼容;比如目标集合有_id: ObjectId,但你的$group输出_id: { year: 2024, month: 7 },就会报错Cannot create field '_id' in element {year: 2024}
真正难的不是怎么写,而是想清楚:你要的“合并”,到底是数组拼接、数值累加,还是结构对齐?选错阶段,后面全是补丁。

















