Express+Mongoose实现MongoDB月度数据统计与占比计算优化
需求说明
我的MongoDB数据库中有如下格式的数据:
{ id: 'ran1', code: 'ABC1', createdAt: 'Sep 1 2022', count: 5 } { id: 'ran2', code: 'ABC1', createdAt: 'Sep 2 2022', count: 3 } { id: 'ran3', code: 'ABC2', createdAt: 'Sep 1 2022', count: 2 } { id: 'ran4', code: 'ABC1', createdAt: 'Oct 1 2022', count: 1 } { id: 'ran5', code: 'ABC1', createdAt: 'Oct 2 2022', count: 2 } { id: 'ran6', code: 'ABC2', createdAt: 'Oct 1 2022', count: 1 }
我需要筛选出10月的所有数据,同时计算每个code的当月总count,以及占比公式:(当月总count - 上月总count)/当月总count * 100,期望输出格式如下:
{code: 'ABC1', totalCount: 3 , percent: (3-8)/3 * 100 } {code: 'ABC2', totalCount: 1, percent: -100}
目前我通过两次聚合查询再做映射匹配的方式实现,但觉得有更优方案,现有代码如下:
const { filterDate, shop } = req.query; const splittedFilter = filterDate.split("-"); const query = { shopUrl: { $regex: shop, $options: "i" }, createdAt: { $gte: new Date(splittedFilter[0]), $lte: new Date(splittedFilter[1]), }, }; const currentCodes = await BlockedCode.aggregate([ { $match: query, }, { $group: { _id: "$discountCode", totalCount: { $sum: "$count" }, }, }, ]); const prevQuery = { shopUrl: { $regex: shop, $options: "i" }, createdAt: { $gte: new Date(splittedFilter[2]), $lte: new Date(splittedFilter[3]), }, }; const previousCodes = await BlockedCode.aggregate([ { $match: prevQuery, }, { $group: { _id: "$discountCode", totalCount: { $sum: "$count" }, }, }, ]); const result = currentCodes.map((code) => { const foundPrevCode = previousCodes.find((i) => i._id === code._id); if (foundPrevCode?._id) { const prevCount = foundPrevCode?.totalCount; const currCount = code?.totalCount; const difference = currCount - prevCount; const percentage = (difference / currCount) * 100; return { ...code, percentage }; } else { return { ...code, percentage: 100 }; } });
优化方案:单次聚合管道实现
可以通过一次聚合查询完成所有计算,避免两次数据库请求和客户端侧的匹配逻辑,具体实现步骤如下:
- 匹配目标月份及上月数据:筛选出当前月和上月的所有符合条件的数据
- 按code分组,区分当月和上月的count总和:分组后用条件累加分别计算当月和上月的总count
- 过滤当月有效数据:仅保留当月有count的code
- 计算占比并格式化输出:基于分组结果计算占比,同时处理上月无数据的情况
具体聚合代码如下:
const { filterDate, shop } = req.query; const splittedFilter = filterDate.split("-"); // 解析当前月和上月的日期范围 const currStart = new Date(splittedFilter[0]); const currEnd = new Date(splittedFilter[1]); const prevStart = new Date(splittedFilter[2]); const prevEnd = new Date(splittedFilter[3]); const result = await BlockedCode.aggregate([ // 第一步:匹配当前月和上月的所有数据 { $match: { shopUrl: { $regex: shop, $options: "i" }, createdAt: { $gte: prevStart, $lte: currEnd } } }, // 第二步:按code分组,分别计算当月和上月的总count { $group: { _id: "$code", // 注意:需与数据库实际字段名一致,原代码用的是$discountCode totalCount: { $sum: { $cond: [ { $and: [ { $gte: ["$createdAt", currStart] }, { $lte: ["$createdAt", currEnd] } ] }, "$count", 0 ] } }, prevTotalCount: { $sum: { $cond: [ { $and: [ { $gte: ["$createdAt", prevStart] }, { $lte: ["$createdAt", prevEnd] } ] }, "$count", 0 ] } } } }, // 第三步:过滤出当月有数据的code { $match: { totalCount: { $gt: 0 } } }, // 第四步:计算占比并格式化输出字段 { $project: { _id: 0, code: "$_id", totalCount: 1, percent: { $cond: [ { $eq: ["$prevTotalCount", 0] }, 100, // 上月无数据时占比为100% { $multiply: [ { $divide: [ { $subtract: ["$totalCount", "$prevTotalCount"] }, "$totalCount" ] }, 100 ] } ] } } } ]);
优化点说明
- 减少数据库请求:从两次聚合查询缩减为一次,降低网络开销和数据库负载
- 逻辑内聚:所有计算逻辑在数据库侧完成,避免客户端侧的数组遍历匹配,数据量越大性能提升越明显
- 容错一致:通过
$cond处理上月无数据的场景,返回100%占比,与原业务逻辑完全对齐
内容的提问来源于stack exchange,提问作者Shahamar Rahman Himel
相关产品推荐
相关产品推荐

