如何在MongoDB中通过单查询/聚合实现多条件多维度计数
MongoDB Single Aggregation Query for Multi-Time-Dimension & Status User Counts
嘿,这个需求完全可以用MongoDB的聚合管道一次性搞定,不用跑多次查询!下面我会一步步拆解逻辑,最后给出完整的查询代码,刚好能输出你想要的嵌套格式。
核心思路
我们会通过以下几个聚合阶段来实现:
- 过滤目标状态:先筛选出状态为
pending、completed、open的用户文档,排除无关数据。 - 标记时间维度:给每个文档添加一个
time_dimension字段,判断它属于today/yesterday/last_week/last_month中的哪一类。 - 分组统计数量:按时间维度+状态组合分组,统计每组的用户数。
- 重塑结果结构:将统计结果转换成你期望的嵌套键值对格式。
完整聚合查询代码
db.users.aggregate([ // 阶段1:过滤出目标状态的用户 { $match: { status: { $in: ["pending", "completed", "open"] } } }, // 阶段2:标记每个文档所属的时间维度 { $addFields: { time_dimension: { $switch: { branches: [ // 今日:从今天0点到明天0点 { case: { $and: [ { $gte: ["$createdAt", { $startOfDay: new Date() }] }, { $lt: ["$createdAt", { $add: [{ $startOfDay: new Date() }, 86400000] }] } ] }, then: "today" }, // 昨日:昨天0点到今天0点 { case: { $and: [ { $gte: ["$createdAt", { $subtract: [{ $startOfDay: new Date() }, 86400000] }] }, { $lt: ["$createdAt", { $startOfDay: new Date() }] } ] }, then: "yesterday" }, // 上周:ISO标准的上周(周一到周日) { case: { $and: [ { $eq: [{ $isoWeek: "$createdAt" }, { $subtract: [{ $isoWeek: new Date() }, 1] }] }, { $eq: [{ $isoYear: "$createdAt" }, { $isoYear: new Date() }] } ] }, then: "last_week" }, // 上月:处理跨年情况(比如1月的上月是去年12月) { case: { $let: { vars: { currentMonth: { $month: new Date() }, currentYear: { $year: new Date() }, docMonth: { $month: "$createdAt" }, docYear: { $year: "$createdAt" } }, in: { $or: [ // 非1月的情况:上月是当前月-1,年份相同 { $and: [ { $ne: ["$$currentMonth", 1] }, { $eq: ["$$docMonth", { $subtract: ["$$currentMonth", 1] }] }, { $eq: ["$$docYear", "$$currentYear"] } ] }, // 1月的情况:上月是12月,年份是当前年-1 { $and: [ { $eq: ["$$currentMonth", 1] }, { $eq: ["$$docMonth", 12] }, { $eq: ["$$docYear", { $subtract: ["$$currentYear", 1] }] } ] } ] } } }, then: "last_month" } ], // 不属于以上维度的文档标记为other(可选,你可以根据需求调整) default: "other" } } } }, // 阶段3:按时间维度+状态分组统计数量 { $group: { _id: { time_dimension: "$time_dimension", status: "$status" }, count: { $sum: 1 } } }, // 阶段4:将同一时间维度的状态统计合并为对象 { $group: { _id: "$_id.time_dimension", stats: { $push: { k: "$_id.status", v: "$count" } } } }, // 阶段5:将stats数组转为键值对对象 { $project: { _id: 0, dimension: "$_id", data: { $arrayToObject: "$stats" } } }, // 阶段6:将所有维度的结果合并为顶层对象 { $group: { _id: null, result: { $push: { k: "$dimension", v: "$data" } } } }, // 阶段7:输出最终格式 { $replaceRoot: { newRoot: { $arrayToObject: "$result" } } } ])
预期结果格式
执行后你会得到类似这样的输出:
{ "today": { "pending": 10, "completed": 25, "open": 18 }, "yesterday": { "pending": 8, "completed": 30, "open": 15 }, "last_week": { "pending": 45, "completed": 120, "open": 88 }, "last_month": { "pending": 160, "completed": 420, "open": 310 }, "other": { // 不属于以上维度的统计(如果有的话) } }
注意事项
- 假设你的用户文档中存储时间的字段是
createdAt,如果是其他字段(比如updatedAt),记得替换成对应的字段名。 - 时间维度的判断逻辑可以根据你的业务需求调整(比如上周的定义是自然周还是过去7天?上面用的是ISO标准周,你可以改成过去7天的范围)。
- 如果某个时间维度下没有对应状态的用户,该状态不会出现在结果中,你可以根据需要添加默认值(比如用
$mergeObjects结合默认对象来补全)。
内容的提问来源于stack exchange,提问作者NitinD
相关产品推荐
相关产品推荐

