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
相关产品推荐
相关产品推荐

