MongoDB聚合管道:按type原地分组嵌套数组procedures
MongoDB聚合管道实现嵌套数组按type原地分组
需求回顾
将每个包含airport、country和procedures数组的文档,转换为procedures字段变为以type为键、对应元素数组为值的对象,且保留原文档所有字段,不混合不同文档的数据。
输入输出示例
输入文档
{ "_id": ObjectId("60d21b4667d0d8992e610c85"), "airport": "JFK", "country": "USA", "procedures": [ { "type": "departure", "details": "D1" }, { "type": "arrival", "details": "A1" }, { "type": "departure", "details": "D2" } ] }
输出文档
{ "_id": ObjectId("60d21b4667d0d8992e610c85"), "airport": "JFK", "country": "USA", "procedures": { "departure": [ { "type": "departure", "details": "D1" }, { "type": "departure", "details": "D2" } ], "arrival": [ { "type": "arrival", "details": "A1" } ] } }
方案一:基于$unwind+$group的通用实现
适合处理大型数组,兼容性好:
db.collection.aggregate([ // 拆分procedures数组,保留空数组/无该字段的文档 { $unwind: { path: "$procedures", preserveNullAndEmptyArrays: true } }, // 按原文档ID、基础字段及procedure类型分组,收集同类型元素 { $group: { _id: { docId: "$_id", airport: "$airport", country: "$country", type: "$procedures.type" }, procedureList: { $push: "$procedures" } } }, // 回到原文档维度,整理为键值对数组 { $group: { _id: "$_id.docId", airport: { $first: "$_id.airport" }, country: { $first: "$_id.country" }, proceduresKV: { $push: { k: "$_id.type", v: "$procedureList" } } } }, // 将键值对数组转为对象,替换原procedures字段 { $addFields: { procedures: { $arrayToObject: "$proceduresKV" }, proceduresKV: "$$REMOVE" // 清理临时字段 } } ])
各阶段说明
- $unwind:将每个文档的
procedures数组拆分为独立文档,确保后续能按type分组,preserveNullAndEmptyArrays避免丢失无procedures的文档。 - 第一个$group:通过
docId(原文档_id)确保分组范围限定在单个文档内,同时按type收集同类型的procedure元素。 - 第二个$group:回归原文档维度,将同文档内的各类型分组结果整理为
{k: type, v: 元素数组}的格式,方便转成对象。 - $addFields:用
$arrayToObject将键值对数组转为目标对象,替换原procedures字段并清理临时数据。
方案二:基于$reduce的高效实现
无需拆分数组,性能更优,适合中小型数组:
db.collection.aggregate([ { $addFields: { procedures: { $arrayToObject: { $reduce: { input: "$procedures", initialValue: [], in: { $let: { vars: { // 检查当前type是否已在结果数组中存在 typeIndex: { $indexOfArray: ["$$value.k", "$$this.type"] } }, in: { $cond: { if: { $ne: ["$$typeIndex", -1] }, // 已存在则追加元素到对应数组 then: { $concatArrays: [ { $slice: ["$$value", 0, "$$typeIndex"] }, [{ k: "$$this.type", v: { $concatArrays: ["$$value.$$typeIndex.v", ["$$this"]] } }], { $slice: ["$$value", { $add: ["$$typeIndex", 1] }] } ] }, // 不存在则新增键值对 else: { $concatArrays: ["$$value", [{ k: "$$this.type", v: ["$$this"] }]] } } } } } } } } } } ])
说明
用$reduce遍历procedures数组,维护一个键值对数组:
- 对每个元素,检查其
type是否已在数组中存在 - 存在则将当前元素追加到对应类型的数组中
- 不存在则新增一个
{k: type, v: [当前元素]}的键值对 - 最后用
$arrayToObject将数组转为目标对象
错误原因分析
你之前的代码丢失airport结构且混合数据,是因为$group阶段没有以**原文档的唯一标识(如_id)**作为分组键,导致跨文档的type被合并,同时没有保留原文档的基础字段。上述方案通过绑定原文档ID,确保每个文档独立处理,完全避免了这个问题。
内容的提问来源于stack exchange,提问作者davidlav
相关产品推荐
相关产品推荐

