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

MongoDB聚合按月份统计:生成含空月份时间线及多字段分组

MongoDB聚合生成带空月份的时间线数据方案

需求说明

我有一个存储实体的MongoDB集合,想通过聚合操作替代后端游标循环处理(后者性能很差),生成包含指定时间范围内所有月份的时间线数据——即使某些月份没有数据,也要保留键并设值为0。同时希望能在同一聚合中按不同日期字段(如created_at、updated_at)分别统计。

当前已能用$dateToString获取所需日期格式:

{ $dateToString: { format: '%Y-%m', date: '$created_at' } }

期望输出结构(单字段示例):

"timeline": {
   "by_month": {
        "2023-04": 1,
        "2023-03": 1,
        "2023-02": 0,
        "2023-01": 1,
        "2022-12": 0,
        "2022-11": 0,
        "2022-10": 0,
        "2022-09": 0,
        "2022-08": 0,
        "2022-07": 0,
        "2022-06": 0,
        "2022-05": 0
    }
}

核心难点

  • 将$dateToString的结果作为MongoDB对象的动态键
  • 为无数据的月份自动生成键,值设为0

单字段(created_at)时间线实现

以示例文档为例:

[
  {
    "key": 1,
    "created_at": "2023-04-02T11:53:19.779+00:00"
  },
  {
    "key": 2,
    "created_at": "2023-03-02T11:53:19.779+00:00"
  },
  {
    "key": 3,
    "created_at": "2023-01-02T11:53:19.779+00:00"
  }
]

聚合管道代码

[
  // 1. 统计现有数据的月份计数
  {
    $group: {
      _id: { $dateToString: { format: "%Y-%m", date: "$created_at" } },
      count: { $sum: 1 }
    }
  },
  // 2. 将统计结果转成数组格式,方便后续合并
  {
    $group: {
      _id: null,
      existingData: { $push: { k: "$_id", v: "$count" } }
    }
  },
  // 3. 生成目标时间范围内的所有月份列表(这里是2022-05到2023-04)
  {
    $addFields: {
      allMonths: [
        "2022-05", "2022-06", "2022-07", "2022-08", "2022-09", "2022-10",
        "2022-11", "2022-12", "2023-01", "2023-02", "2023-03", "2023-04"
      ]
    }
  },
  // 4. 遍历所有月份,匹配现有统计数据,无数据则设为0
  {
    $addFields: {
      filledData: {
        $map: {
          input: "$allMonths",
          as: "month",
          in: {
            k: "$$month",
            v: {
              $ifNull: [
                { $arrayElemAt: [ "$existingData.v", { $indexOfArray: [ "$existingData.k", "$$month" ] } ] },
                0
              ]
            }
          }
        }
      }
    }
  },
  // 5. 将数组转成目标对象格式,包装成timeline结构
  {
    $project: {
      _id: 0,
      timeline: {
        by_month: { $arrayToObject: "$filledData" }
      }
    }
  }
]

关键步骤说明

  • 生成完整月份列表:需要提前指定目标时间范围的所有月份(可通过代码动态生成后传入聚合)
  • $arrayToObject:解决动态键的问题,将[{k: "2023-04", v:1}, ...]格式的数组转成对象
  • $map + $ifNull:遍历所有月份,匹配不到统计数据时自动设为0

多字段(created_at/updated_at)时间线实现

针对包含created_at和updated_at的文档:

[
  {
    "key": 1,
    "created_at": ISODate("2023-04-02T13:53:19.779Z"),
    "updated_at": ISODate("2023-04-02T13:53:19.779Z")
  },
  {
    "key": 2,
    "created_at": ISODate("2023-03-02T12:53:19.779Z"),
    "updated_at": ISODate("2023-04-02T13:53:19.779Z")
  },
  {
    "key": 3,
    "created_at": ISODate("2023-01-02T12:53:19.779Z"),
    "updated_at": ISODate("2023-04-02T13:53:19.779Z")
  },
  {
    "key": 4,
    "created_at": ISODate("2023-04-02T12:53:19.779Z"),
    "updated_at": ISODate("2023-04-02T13:53:19.779Z")
  }
]

聚合管道代码

[
  // 1. 同时统计created和updated的月份计数
  {
    $facet: {
      createdStats: [
        {
          $group: {
            _id: { $dateToString: { format: "%Y-%m", date: "$created_at" } },
            count: { $sum: 1 }
          }
        },
        { $group: { _id: null, data: { $push: { k: "$_id", v: "$count" } } } }
      ],
      updatedStats: [
        {
          $group: {
            _id: { $dateToString: { format: "%Y-%m", date: "$updated_at" } },
            count: { $sum: 1 }
          }
        },
        { $group: { _id: null, data: { $push: { k: "$_id", v: "$count" } } } }
      ]
    }
  },
  // 2. 提取统计数据,生成完整月份列表
  {
    $addFields: {
      createdData: { $arrayElemAt: [ "$createdStats.data", 0 ] },
      updatedData: { $arrayElemAt: [ "$updatedStats.data", 0 ] },
      allMonths: [
        "2022-05", "2022-06", "2022-07", "2022-08", "2022-09", "2022-10",
        "2022-11", "2022-12", "2023-01", "2023-02", "2023-03", "2023-04"
      ]
    }
  },
  // 3. 分别填充两个字段的空月份数据
  {
    $addFields: {
      createdFilled: {
        $map: {
          input: "$allMonths",
          as: "month",
          in: {
            k: "$$month",
            v: {
              $ifNull: [
                { $arrayElemAt: [ "$createdData.v", { $indexOfArray: [ "$createdData.k", "$$month" ] } ] },
                0
              ]
            }
          }
        }
      },
      updatedFilled: {
        $map: {
          input: "$allMonths",
          as: "month",
          in: {
            k: "$$month",
            v: {
              $ifNull: [
                { $arrayElemAt: [ "$updatedData.v", { $indexOfArray: [ "$updatedData.k", "$$month" ] } ] },
                0
              ]
            }
          }
        }
      }
    }
  },
  // 4. 转成对象并包装成目标结构
  {
    $project: {
      _id: 0,
      timeline: {
        created_by_month: { $arrayToObject: "$createdFilled" },
        updated_by_month: { $arrayToObject: "$updatedFilled" }
      }
    }
  }
]

关键步骤说明

  • $facet:在同一聚合中并行处理两个统计任务,分别得到created_at和updated_at的月份计数
  • 后续逻辑:和单字段方案一致,填充空月份后转成对象格式

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 16:23:08