MongoDB投票结果聚合查询优化及数据结构合理性咨询
投票结果聚合统计优化与数据库结构咨询
现有集合结构
poll集合文档示例
{ "_id": { "$oid": "636027704f7a15587ef74f26" }, "question": "question 1", "ended": false, "options": [ { "id": "1", "option": "option 1" }, { "id": "2", "option": "option 2" }, { "id": "3", "option": "option 3" } ] }
vote集合文档示例
{ "_id": { "$oid": "635ed3210acbf9fd14af8fd1" }, "poll_id": "636027704f7a15587ef74f26", "poll_option_id": "1", "user_id": "1" }
当前聚合查询及结果
当前使用的聚合查询:
db.vote.aggregate( [ { $addFields: { poll_id: { "$toObjectId": "$poll_id" } }, }, { $lookup: { from: "poll", localField: "poll_id", foreignField: "_id", as: "details" } }, { $group: { _id: { poll_id: "$poll_id", poll_option_id: "$poll_option_id" }, details: { $first: "$details" }, count: { $sum: 1 } } }, { $addFields: { question: { $arrayElemAt: ["$details.question", 0] } } }, { $addFields: { options: { $arrayElemAt: ["$details.options", 0] } } }, { $group: { _id: "$_id.poll_id", poll_id: { $first: "$_id.poll_id" }, question: { $first: "$question" }, options: { $first: "$options" }, optionsGrouped: { $push: { id: "$_id.poll_option_id", count: "$count" } }, count: { $sum: "$count" } } } ] )
返回结果:
{ _id: ObjectId("636027704f7a15587ef74f26"), poll_id: ObjectId("636027704f7a15587ef74f26"), question: 'question 1', options: [ { id: '1', option: 'option 1' }, { id: '2', option: 'option 2' }, { id: '3', option: 'option 3' } ], optionsGrouped: [ { id: '1', count: 2 }, { id: '2', count: 1 } ], count: 3 }
需求
希望将结果优化为合并选项与投票统计的形式,包含未获票选项的count:0,目标结果如下:
{ _id: ObjectId("636027704f7a15587ef74f26"), poll_id: ObjectId("636027704f7a15587ef74f26"), question: 'question 1', optionsGrouped: [ { id: '1', option: 'option 1', count: 2 }, { id: '2', option: 'option 2', count: 1 }, { id: '3', option: 'option 3', count: 0 } ], count: 4 }
同时咨询当前数据库结构是否合理,有无更优设计方案。
解决方案
优化后的聚合查询
从poll集合发起查询能确保所有选项被纳入统计,包括未获票的选项:
db.poll.aggregate([ // 关联vote集合并统计每个选项的票数 { $lookup: { from: "vote", let: { poll_id: "$_id", option_ids: "$options.id" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: [{ $toObjectId: "$poll_id" }, "$$poll_id"] }, { $in: "$poll_option_id", "$$option_ids" } ] } } }, { $group: { _id: "$poll_option_id", count: { $sum: 1 } } } ], as: "vote_stats" } }, // 将投票统计转为键值对映射,方便匹配 { $addFields: { vote_map: { $arrayToObject: { $map: { input: "$vote_stats", as: "stat", in: { k: "$$stat._id", v: "$$stat.count" } } } } } }, // 合并选项与统计数据,未获票选项count设为0 { $addFields: { optionsGrouped: { $map: { input: "$options", as: "opt", in: { id: "$$opt.id", option: "$$opt.option", count: { $ifNull: [ { $getField: { field: "$$opt.id", input: "$vote_map" } }, 0 ] } } } }, // 计算总票数 count: { $sum: { $map: { input: "$optionsGrouped", as: "g", in: "$$g.count" } } } } }, // 移除冗余字段 { $project: { options: 0, vote_stats: 0, vote_map: 0 } } ])
查询说明
- 从poll集合发起查询:确保所有投票选项都被统计,不会遗漏未获票的选项。
- 子管道统计票数:通过
$lookup的子聚合,精准统计当前投票下每个选项的得票数。 - 键值对映射:把投票统计结果转为对象,快速匹配选项ID对应的票数。
- 合并选项与统计:遍历poll的options数组,匹配票数,无匹配则默认设为0。
- 总票数计算与字段清理:对所有选项的count求和得到总票数,最后移除不需要的中间字段。
数据库结构分析与优化建议
当前结构合理性
当前的poll-vote分离结构是合理的,属于典型的关联式设计:
- poll集合存储投票元数据(问题、选项、状态),结构清晰易维护。
- vote集合存储单条投票记录,避免了在poll中频繁更新数组的性能问题,同时能完整保留投票历史(比如用户是否重复投票、投票时间等扩展字段)。
潜在优化方案
根据业务场景不同,可考虑以下优化:
1. 嵌入投票统计到poll集合(读写权衡)
如果投票结果读取频率远高于写入,且不需要保留详细投票历史,可以在poll集合中嵌入vote_counts字段,每次投票时用$inc原子更新对应选项的计数:
{ "_id": ObjectId("636027704f7a15587ef74f26"), "question": "question 1", "ended": false, "options": [ { "id": "1", "option": "option 1" }, { "id": "2", "option": "option 2" }, { "id": "3", "option": "option 3" } ], "vote_counts": { "1": 2, "2": 1, "3": 0 }, "total_votes": 3 }
- 优点:读取投票结果无需聚合,性能极高。
- 缺点:无法追踪单个用户的投票记录;高并发投票时可能存在锁竞争。
2. 为vote集合添加索引
当前vote集合的查询依赖poll_id和poll_option_id,建议添加复合索引提升聚合效率:
db.vote.createIndex({ poll_id: 1, poll_option_id: 1 })
同时将vote集合中的poll_id字段改为ObjectId类型,避免聚合时的$toObjectId转换开销。
3. 扩展vote集合字段
如果需要追踪投票时间、用户设备等信息,可以在vote文档中添加created_at、device_info等字段,便于后续数据分析。
内容的提问来源于stack exchange,提问作者Sherif Mo Shalaby
相关产品推荐
相关产品推荐

