MongoDB三层嵌套数组聚合分组:合并tunerDetails至对应session
解决MongoDB三层嵌套数组的聚合分组问题
我完全懂你的困扰:直接三层unwind后分组要么丢失上层的linearTvChannels信息,要么会产生大量重复的session条目。下面是一套完整的聚合方案,既能精准筛选出signalLocked=0的tunerDetails,又能把它们合并到所属的session数组中,还能完整保留linearTvChannels的所有信息:
完整聚合管道示例
假设你的集合名为tv_data,文档结构大致如下:
{ "_id": ObjectId("xxx"), "otherTopField": "some_value", "linearTvChannels": [ { "channelId": "ch_001", "channelName": "体育频道", "sessions": [ { "sessionId": "ses_001", "startTime": ISODate("2024-05-01T18:00:00Z"), "tunerDetails": [ { "tunerId": "tun_001", "signalLocked": 0, "signalStrength": -50 }, { "tunerId": "tun_002", "signalLocked": 1, "signalStrength": -30 } ] } ] } ] }
对应的聚合管道如下:
db.tv_data.aggregate([ // 1. 展开最外层的linearTvChannels数组,保留顶层文档标识 { $unwind: "$linearTvChannels" }, // 2. 展开当前频道下的sessions数组,保留频道和会话信息 { $unwind: "$linearTvChannels.sessions" }, // 3. 展开当前会话下的tunerDetails数组,准备筛选 { $unwind: "$linearTvChannels.sessions.tunerDetails" }, // 4. 筛选出signalLocked=0的tunerDetails条目 { $match: { "linearTvChannels.sessions.tunerDetails.signalLocked": 0 } }, // 5. 按「原文档ID+频道ID+会话ID」分组,合并同一会话的目标tunerDetails { $group: { _id: { docId: "$_id", channelId: "$linearTvChannels.channelId", sessionId: "$linearTvChannels.sessions.sessionId" }, // 保留频道的其他字段(按需添加) channelName: { $first: "$linearTvChannels.channelName" }, // 保留会话的其他字段(按需添加) startTime: { $first: "$linearTvChannels.sessions.startTime" }, // 收集符合条件的tunerDetails到数组 tunerDetails: { $push: "$linearTvChannels.sessions.tunerDetails" }, // 保留顶层文档的其他字段 otherTopField: { $first: "$otherTopField" } } }, // 6. 按「原文档ID+频道ID」分组,合并同一频道下的会话 { $group: { _id: { docId: "$_id.docId", channelId: "$_id.channelId" }, channelName: { $first: "$channelName" }, otherTopField: { $first: "$otherTopField" }, // 收集当前频道下的所有有效会话 sessions: { $push: { sessionId: "$_id.sessionId", startTime: "$startTime", tunerDetails: "$tunerDetails" } } } }, // 7. 还原顶层文档结构,合并处理后的频道数组 { $group: { _id: "$_id.docId", otherTopField: { $first: "$otherTopField" }, linearTvChannels: { $push: { channelId: "$_id.channelId", channelName: "$channelName", sessions: "$sessions" } } } } ])
关键思路说明
- 分层分组,逐层还原:从最底层的
tunerDetails开始,先按「文档+频道+会话」的唯一标识分组,合并同一会话下的目标条目;再按「文档+频道」分组,合并同一频道下的会话;最后按原文档ID分组,还原顶层结构,全程不会丢失任何上层信息。 - 保留非数组字段:在每个
$group阶段用$first保留频道名称、会话时间这类非数组字段,确保这些信息不会被分组操作覆盖。 - 精准过滤无效数据:在展开所有数组后用
$match筛选目标条目,避免无效数据进入后续分组流程。
可选优化:用数组操作替代多层unwind
如果你的MongoDB版本在4.2以上,可以直接用$map和$filter在数组内完成筛选,避免多次unwind的性能开销:
db.tv_data.aggregate([ { $addFields: { linearTvChannels: { $filter: { input: { $map: { input: "$linearTvChannels", as: "channel", in: { // 保留频道所有原有字段 ..."$$channel", sessions: { $filter: { input: { $map: { input: "$$channel.sessions", as: "session", in: { // 保留会话所有原有字段 ..."$$session", // 筛选signalLocked=0的tunerDetails tunerDetails: { $filter: { input: "$$session.tunerDetails", cond: { $eq: ["$$this.signalLocked", 0] } } } } } }, // 过滤掉没有有效tunerDetails的会话 cond: { $gt: [{ $size: "$$this.tunerDetails" }, 0] } } } } } }, // 过滤掉没有有效会话的频道 cond: { $gt: [{ $size: "$$this.sessions" }, 0] } } } } } ])
这种方式不需要展开数组,直接在原结构内完成筛选和过滤,性能更优,代码也更简洁。
内容的提问来源于stack exchange,提问作者eric_balawejder
相关产品推荐
相关产品推荐

