MongoDB聚合$bucket边界下水果统计的多方案实现详解
MongoDB聚合框架:年龄分桶+水果统计的两种实现方案
基于给定的用户集合,我们需要按≤30岁、40-50岁、50岁以上三个年龄区间分桶,每个桶返回用户总数和各水果的统计数量。以下是两种不同的聚合管道实现方案,每一步都做详细解释。
原始集合结构
[ { _id: 123, name: "john", age: 30, fruit: "apple" }, { _id: 345, name: "moore", age: 45, fruit: "mango" }, { _id: 545, name: "carl", age: 30, fruit: "grape" }, { _id: 96, name: "shelby", age: 25, fruit: "apple" }, { _id: 86, name: "loris", age: 48, fruit: "mango" }, { _id: 76, name: "carl", age: 55, fruit: "grape" } ]
方案1:先分桶,再嵌套统计水果
聚合管道代码
db.users.aggregate([ // 阶段1:按指定年龄边界分桶 { $bucket: { groupBy: "$age", boundaries: [0, 31, 40, 51], default: "more than 50", output: { "userCount": { $sum: 1 }, "fruits": { $push: "$fruit" } } } }, // 阶段2:生成水果统计数组 { $addFields: { fruitsLie: { $map: { input: { $setUnion: ["$fruits"] }, as: "fruit", in: { $arrayToObject: [[{ k: "$$fruit", v: { $size: { $filter: { input: "$fruits", cond: { $eq: ["$$this", "$$fruit"] } } } } }]] } } } } }, // 阶段3:清理临时字段 { $project: { fruits: 0 } } ])
阶段详细解释
$bucket 分桶阶段
groupBy: "$age":指定用age字段作为分桶依据boundaries: [0, 31, 40, 51]:定义分桶的数值边界,对应三个有效区间:- 0 ≤ 年龄 < 31 → 桶ID为
0(覆盖≤30岁的用户) - 40 ≤ 年龄 < 51 → 桶ID为
40(覆盖40-50岁的用户) - 年龄 ≥51 → 进入
default指定的桶
- 0 ≤ 年龄 < 31 → 桶ID为
default: "more than 50":处理所有不在boundaries范围内的年龄(这里是50岁以上)output:每个桶输出两个字段:userCount:用$sum:1统计桶内的用户总数(每一条数据加1)fruits:用$push把桶内所有用户的fruit字段收集成一个数组,比如["apple", "grape", "apple"]
$addFields 生成水果统计
$setUnion: ["$fruits"]:从fruits数组中提取不重复的水果名称,避免重复统计$map:遍历每个不重复的水果,生成统计对象:$filter:从fruits数组中筛选出等于当前水果的元素$size:统计筛选后的元素数量,得到该水果的用户数$arrayToObject:把{k: 水果名, v: 数量}转换成{水果名: 数量}的对象,再套一层数组,匹配需求的fruitsLie格式
$project 清理字段
- 移除临时生成的
fruits数组,只保留最终需要的_id、userCount、fruitsLie字段
- 移除临时生成的
方案2:先细分组,再按桶聚合
聚合管道代码
db.users.aggregate([ // 阶段1:按年龄桶+水果分组统计 { $group: { _id: { ageBucket: { $switch: { branches: [ { case: { $lte: ["$age", 30] }, then: 0 }, { case: { $and: [{ $gt: ["$age", 30] }, { $lte: ["$age", 50] }] }, then: 40 }, { case: { $gt: ["$age", 50] }, then: "more than 50" } ] } }, fruit: "$fruit" }, count: { $sum: 1 } } }, // 阶段2:按年龄桶合并统计结果 { $group: { _id: "$_id.ageBucket", userCount: { $sum: "$count" }, fruitsLie: { $push: { $arrayToObject: [[{ k: "$_id.fruit", v: "$count" }]] } } } }, // 阶段3:按桶ID排序(可选) { $sort: { _id: 1 } } ])
阶段详细解释
$group 细粒度分组
_id是复合键:包含ageBucket(手动划分的年龄区间)和fruit(用户喜欢的水果)$switch:直接定义三个年龄区间的桶ID,完全匹配需求:- 年龄≤30 → 桶ID为
0 - 年龄>30且≤50 → 桶ID为
40 - 年龄>50 → 桶ID为
more than 50
- 年龄≤30 → 桶ID为
count: { $sum:1 }:统计每个(年龄桶+水果)组合的用户数量,比如{_id: {ageBucket:0, fruit:"apple"}, count:2}
$group 按桶聚合
_id: "$_id.ageBucket":按年龄桶重新分组,把同一桶内的水果统计结果合并userCount: { $sum: "$count" }:把桶内所有水果的统计数量相加,得到该桶的总用户数fruitsLie: { $push: ... }:把每个(水果:数量)的对象推入数组,用$arrayToObject转换格式,生成符合要求的fruitsLie数组
$sort 排序(可选)
- 按
_id升序排列,让结果顺序更直观:0→40→more than 50
- 按
两种方案对比
- 方案1:逻辑更直观,先完成分桶再处理桶内数据,适合新手理解
$bucket的基础用法,但如果桶内用户数量大,生成的fruits数组会占用较多内存 - 方案2:先做细粒度分组,再向上聚合,避免生成大数组,数据量大时性能更优,适合实际生产环境
内容的提问来源于stack exchange,提问作者Mohammad Farhan
相关产品推荐
相关产品推荐

