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

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

阶段详细解释

  1. $bucket 分桶阶段

    • groupBy: "$age":指定用age字段作为分桶依据
    • boundaries: [0, 31, 40, 51]:定义分桶的数值边界,对应三个有效区间:
      • 0 ≤ 年龄 < 31 → 桶ID为0(覆盖≤30岁的用户)
      • 40 ≤ 年龄 < 51 → 桶ID为40(覆盖40-50岁的用户)
      • 年龄 ≥51 → 进入default指定的桶
    • default: "more than 50":处理所有不在boundaries范围内的年龄(这里是50岁以上)
    • output:每个桶输出两个字段:
      • userCount:用$sum:1统计桶内的用户总数(每一条数据加1)
      • fruits:用$push把桶内所有用户的fruit字段收集成一个数组,比如["apple", "grape", "apple"]
  2. $addFields 生成水果统计

    • $setUnion: ["$fruits"]:从fruits数组中提取不重复的水果名称,避免重复统计
    • $map:遍历每个不重复的水果,生成统计对象:
      • $filter:从fruits数组中筛选出等于当前水果的元素
      • $size:统计筛选后的元素数量,得到该水果的用户数
      • $arrayToObject:把{k: 水果名, v: 数量}转换成{水果名: 数量}的对象,再套一层数组,匹配需求的fruitsLie格式
  3. $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 }
  }
])

阶段详细解释

  1. $group 细粒度分组

    • _id是复合键:包含ageBucket(手动划分的年龄区间)和fruit(用户喜欢的水果)
    • $switch:直接定义三个年龄区间的桶ID,完全匹配需求:
      • 年龄≤30 → 桶ID为0
      • 年龄>30且≤50 → 桶ID为40
      • 年龄>50 → 桶ID为more than 50
    • count: { $sum:1 }:统计每个(年龄桶+水果)组合的用户数量,比如{_id: {ageBucket:0, fruit:"apple"}, count:2}
  2. $group 按桶聚合

    • _id: "$_id.ageBucket":按年龄桶重新分组,把同一桶内的水果统计结果合并
    • userCount: { $sum: "$count" }:把桶内所有水果的统计数量相加,得到该桶的总用户数
    • fruitsLie: { $push: ... }:把每个(水果:数量)的对象推入数组,用$arrayToObject转换格式,生成符合要求的fruitsLie数组
  3. $sort 排序(可选)

    • 按_id升序排列,让结果顺序更直观:0 → 40 → more than 50

两种方案对比

  • 方案1:逻辑更直观,先完成分桶再处理桶内数据,适合新手理解$bucket的基础用法,但如果桶内用户数量大,生成的fruits数组会占用较多内存
  • 方案2:先做细粒度分组,再向上聚合,避免生成大数组,数据量大时性能更优,适合实际生产环境

内容的提问来源于stack exchange,提问作者Mohammad Farhan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 21:50:33