MongoDB小时级价格数据按区间计算涨跌幅的聚合优化方案咨询
现有实现的核心问题
你当前的方案存在两个明显的缺陷,也是性能差、稳定性低的核心原因:
- 完全依赖history数组的下标取值,要求数组严格按时间倒序、无任何漏采/补采的小时数据,只要出现一次数据缺失,所有区间的涨跌幅计算都会出错
- 查询时会返回整个history数组的全量数据,如果你存储了1年以上的小时数据,单条文档的history数组就会有近万条元素,网络传输、内存解析的开销都非常大
基于Aggregate聚合的优化方案
完全可以通过MongoDB的聚合管道实现需求,所有计算逻辑在数据库层完成,不需要返回全量history数组,同时按时间维度匹配,完全不依赖数组下标,准确率和性能都远高于现有实现。
实现代码
exports.getCollectibleChangeSummary = async (req, res) => { const slug = req.params.slug; // 定义各时间区间对应的毫秒数 const TIME_RANGES = { one_day: 24 * 60 * 60 * 1000, one_week: 7 * 24 * 60 * 60 * 1000, one_month: 30 * 24 * 60 * 60 * 1000, three_month: 90 * 24 * 60 * 60 * 1000, six_month: 180 * 24 * 60 * 60 * 1000, one_year: 365 * 24 * 60 * 60 * 1000 }; try { const result = await MarketPriceHistoric.aggregate([ // 第一步:匹配目标商品,建议给collectibleId字段建索引,大幅提升查询速度 { $match: { collectibleId: slug } }, // 第二步:提取当前价格(history数组第一条为最新价格) { $addFields: { currentPrice: { $ifNull: [{ $first: "$history.value" }, 0] } } }, // 第三步:计算各时间区间的基准价格 { $project: { currentPrice: 1, // 通用逻辑:过滤出对应时间点及之后的所有数据,取最后一条即为最接近时间点的价格 one_day_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.one_day] }] } } } }, one_week_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.one_week] }] } } } }, one_month_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.one_month] }] } } } }, three_month_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.three_month] }] } } } }, six_month_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.six_month] }] } } } }, one_year_price: { $last: { $filter: { input: "$history", cond: { $gte: ["$$this.date", { $subtract: ["$$NOW", TIME_RANGES.one_year] }] } } } } } }, // 第四步:计算涨跌幅,无对应数据返回null { $project: { currentPrice: 1, one_day_change: { $cond: { if: { $gt: ["$one_day_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$one_day_price.value"] }, "$one_day_price.value"] }, 100] }, else: null } }, one_week_change: { $cond: { if: { $gt: ["$one_week_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$one_week_price.value"] }, "$one_week_price.value"] }, 100] }, else: null } }, one_month_change: { $cond: { if: { $gt: ["$one_month_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$one_month_price.value"] }, "$one_month_price.value"] }, 100] }, else: null } }, three_month_change: { $cond: { if: { $gt: ["$three_month_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$three_month_price.value"] }, "$three_month_price.value"] }, 100] }, else: null } }, six_month_change: { $cond: { if: { $gt: ["$six_month_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$six_month_price.value"] }, "$six_month_price.value"] }, 100] }, else: null } }, one_year_change: { $cond: { if: { $gt: ["$one_year_price.value", 0] }, then: { $multiply: [{ $divide: [{ $subtract: ["$currentPrice", "$one_year_price.value"] }, "$one_year_price.value"] }, 100] }, else: null } } } } ]); if (!result.length) { return res.status(404).json({ error: "未找到对应商品的价格数据" }); } // 去掉聚合返回的_id字段 const { _id, ...rest } = result[0]; res.json(rest); } catch (err) { return res.status(400).json({ error: errorHandler(err) }); } };
额外优化建议
- 请务必给
collectibleId字段添加单键索引,$match阶段命中索引后查询耗时会降到毫秒级 - 如果你的业务并发量很高,可以加定时任务每天预计算所有商品的各区间涨跌幅,存入单独的字段,查询时直接读取即可,性能可以再提升10倍以上
- 若history数组长度超过1万条,可以考虑做冷热数据分离,超过1年的历史数据归档到单独的集合中,减少主集合的文档大小
内容的提问来源于stack exchange,提问作者alienbuild
相关产品推荐
相关产品推荐

