You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
        }
    }
])

查询说明

  1. 从poll集合发起查询:确保所有投票选项都被统计,不会遗漏未获票的选项。
  2. 子管道统计票数:通过$lookup的子聚合,精准统计当前投票下每个选项的得票数。
  3. 键值对映射:把投票统计结果转为对象,快速匹配选项ID对应的票数。
  4. 合并选项与统计:遍历poll的options数组,匹配票数,无匹配则默认设为0。
  5. 总票数计算与字段清理:对所有选项的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 10:01:23