MongoDB聚合管道实现多文档数组元素计数及分组回退问题
问题:统计多名玩家特殊单位总数的MongoDB聚合实现
我正在开发一个统计多名玩家特殊单位总数的程序。MongoDB中的文档结构简化后如下(rosterUnits数组长度为0至7,共7种特殊单位):
[ { _id: ObjectId(...), member: { rosterUnits: [ "Unit1", "Unit2", "Unit3", "Unit4"] } }, { _id: ObjectId(...), member: { rosterUnits: [ "Unit1", "Unit3"] } }, ... ]
期望得到的结果如下:
{ _id: ..., result: [ { name: "Unit1", count: 2 }, { name: "Unit2", count: 1 }, { name: "Unit3", count: 2 }, ... { name: "Unit7", count: 0 } ] }
如何通过aggregate pipeline实现该需求?
补充说明(2023年2月7日)
实际文档结构复杂,现有聚合查询已完成公会匹配、成员拆分、关联玩家数据、统计GP等操作:
db.getCollection('guilds').aggregate([ { $match: { 'profile.id': 'jrl9Q-_CRDGdMyNjTQH1rQ' } }, //{ $match: { 'profile.id': { $in : ['jrl9Q-_CRDGdMyNjTQH1rQ', 'Tv_j9nhRTgufvH7C7oUYAA']} } }, { $project: { member: 1, profile: 1 } }, { $unwind: "$member" }, { $lookup: { from: "players", localField: "member.playerId", foreignField: "playerId", pipeline: [ { $project: { profileStat: 1, rosterUnit: { $let: { vars: { gls: ["JABBATHEHUTT:SEVEN_STAR", "JEDIMASTERKENOBI:SEVEN_STAR", "GRANDMASTERLUKE:SEVEN_STAR", "LORDVADER:SEVEN_STAR", "GLREY:SEVEN_STAR", "SITHPALPATINE:SEVEN_STAR", "SUPREMELEADERKYLOREN:SEVEN_STAR"], }, in: { $reduce: { input: "$rosterUnit", initialValue: [], in: { $cond: { if: { $gt: [{ $indexOfArray: ["$$gls", "$$this.definitionId"] }, -1] }, then: { $concatArrays: ["$$value", [{ definitionId: "$$this.definitionId", count: 1 }]] }, else: { $concatArrays: ["$$value", []] } } }, } } } } } } ], as: "member" } }, { $addFields: { member: { $arrayElemAt: ["$member", 0] }, gpStats: { $let: { vars: { member: { $arrayElemAt: ["$member", 0] } }, in: { $reduce: { input: "$$member.profileStat", initialValue: {}, in: { characterGp: { $arrayElemAt: [ "$$member.profileStat.value", { $indexOfArray: [ "$$member.profileStat.nameKey", "STAT_CHARACTER_GALACTIC_POWER_ACQUIRED_NAME" ] } ] }, shipGp: { $arrayElemAt: [ "$$member.profileStat.value", { $indexOfArray: [ "$$member.profileStat.nameKey", "STAT_SHIP_GALACTIC_POWER_ACQUIRED_NAME" ] } ] } } } } } } } }, { $group: { _id: "$profile.id", guildName: { $first: "$profile.name" }, memberCount: { $first: "$profile.memberCount" }, guildGp: { $first: "$profile.guildGalacticPower" }, totalGp: { $sum: { $sum: [{ $toInt: "$gpStats.characterGp" }, { $toInt: "$gpStats.shipGp" }] } }, avgTotalGp: { $avg: { $sum: [{ $toInt: "$gpStats.characterGp" }, { $toInt: "$gpStats.shipGp" }] } }, characterGp: { $sum: { $toInt: "$gpStats.characterGp" } }, shipGp: { $sum: { $toInt: "$gpStats.shipGp" } }, } } ])
我希望在$group阶段新增上述特殊单位计数的字段,若对member.rosterUnit执行$unwind后,如何回到原有的分组结构?
解决方案
你可以通过两次分组实现需求:先按「公会ID+单位ID」分组统计数量并累加GP数据,再按「公会ID」分组聚合最终结果,同时补全count为0的单位。以下是修改后的完整聚合管道:
db.getCollection('guilds').aggregate([ { $match: { 'profile.id': 'jrl9Q-_CRDGdMyNjTQH1rQ' } }, { $project: { member: 1, profile: 1 } }, { $unwind: "$member" }, { $lookup: { from: "players", localField: "member.playerId", foreignField: "playerId", pipeline: [ { $project: { profileStat: 1, rosterUnit: { $let: { vars: { gls: ["JABBATHEHUTT:SEVEN_STAR", "JEDIMASTERKENOBI:SEVEN_STAR", "GRANDMASTERLUKE:SEVEN_STAR", "LORDVADER:SEVEN_STAR", "GLREY:SEVEN_STAR", "SITHPALPATINE:SEVEN_STAR", "SUPREMELEADERKYLOREN:SEVEN_STAR"] }, in: { $reduce: { input: "$rosterUnit", initialValue: [], in: { $cond: { if: { $gt: [{ $indexOfArray: ["$$gls", "$$this.definitionId"] }, -1] }, then: { $concatArrays: ["$$value", [{ definitionId: "$$this.definitionId", count: 1 }]] }, else: "$$value" } } } } } } } } ], as: "member" } }, { $addFields: { member: { $arrayElemAt: ["$member", 0] }, gpStats: { $let: { vars: { member: { $arrayElemAt: ["$member", 0] } }, in: { characterGp: { $arrayElemAt: [ "$$member.profileStat.value", { $indexOfArray: ["$$member.profileStat.nameKey", "STAT_CHARACTER_GALACTIC_POWER_ACQUIRED_NAME"] } ] }, shipGp: { $arrayElemAt: [ "$$member.profileStat.value", { $indexOfArray: ["$$member.profileStat.nameKey", "STAT_SHIP_GALACTIC_POWER_ACQUIRED_NAME"] } ] } } } }, // 预先计算当前玩家的GP数据,避免后续重复计算 playerTotalGp: { $sum: [{ $toInt: "$gpStats.characterGp" }, { $toInt: "$gpStats.shipGp" }] }, playerCharacterGp: { $toInt: "$gpStats.characterGp" }, playerShipGp: { $toInt: "$gpStats.shipGp" } } }, // 展开玩家的特殊单位数组,保留无特殊单位的玩家(防止数据丢失) { $unwind: { path: "$member.rosterUnit", preserveNullAndEmptyArrays: true } }, // 第一次分组:按公会+单位统计数量,同时累加GP数据 { $group: { _id: { guildId: "$profile.id", unitId: { $ifNull: ["$member.rosterUnit.definitionId", "UNKNOWN"] } }, count: { $sum: { $ifNull: ["$member.rosterUnit.count", 0] } }, guildName: { $first: "$profile.name" }, memberCount: { $first: "$profile.memberCount" }, guildGp: { $first: "$profile.guildGalacticPower" }, totalGp: { $sum: "$playerTotalGp" }, avgTotalGp: { $avg: "$playerTotalGp" }, characterGp: { $sum: "$playerCharacterGp" }, shipGp: { $sum: "$playerShipGp" } } }, // 第二次分组:按公会聚合,整理单位统计结果 { $group: { _id: "$_id.guildId", guildName: { $first: "$guildName" }, memberCount: { $first: "$memberCount" }, guildGp: { $first: "$guildGp" }, totalGp: { $first: "$totalGp" }, avgTotalGp: { $first: "$avgTotalGp" }, characterGp: { $first: "$characterGp" }, shipGp: { $first: "$shipGp" }, // 收集已统计的单位数据 unitCounts: { $push: { name: "$_id.unitId", count: "$count" } } } }, // 补全所有7种特殊单位,包括count为0的 { $addFields: { result: { $map: { input: ["JABBATHEHUTT:SEVEN_STAR", "JEDIMASTERKENOBI:SEVEN_STAR", "GRANDMASTERLUKE:SEVEN_STAR", "LORDVADER:SEVEN_STAR", "GLREY:SEVEN_STAR", "SITHPALPATINE:SEVEN_STAR", "SUPREMELEADERKYLOREN:SEVEN_STAR"], as: "unit", in: { name: "$$unit", count: { $let: { vars: { matched: { $first: { $filter: { input: "$unitCounts", cond: { $eq: ["$$this.name", "$$unit"] } } } } }, in: { $ifNull: ["$$matched.count", 0] } } } } } } } }, // 可选:移除临时字段unitCounts { $project: { unitCounts: 0 } } ])
关键步骤说明
- 预先计算玩家GP:在
$addFields阶段新增playerTotalGp等字段,提前计算每个玩家的GP数据,减少后续重复计算。 - 安全展开单位数组:使用
$unwind并设置preserveNullAndEmptyArrays: true,确保没有特殊单位的玩家数据不会被过滤,这类玩家的单位count会被计为0。 - 第一次分组(公会+单位):同时完成单位数量统计和GP数据累加,保证同一公会的GP统计准确。
- 第二次分组(按公会):将单位统计结果收集为数组,同时保留原有的公会信息和GP统计字段,回到你需要的分组结构。
- 补全count为0的单位:通过
$map遍历所有7种特殊单位,用$filter匹配已统计的数量,未匹配到的设为0,生成符合需求的result数组。
内容的提问来源于stack exchange,提问作者avukalov
相关产品推荐
相关产品推荐

