直接用 $datetostring 做同比会出错,因为 mongodb 原生不支持智能日期回退(如 2023-02-29 不存在),需先提取 %m-%d 标准化再拼接年份并兜底非法日期。

为什么直接用 $dateToString 做同比会出错
因为 MongoDB 的日期字段是 ISODate 类型,直接用 $subtract 或加减天数容易跨月/跨年失准(比如 3 月 31 日减 30 天不是 2 月 31 日,而是自动归到 3 月 2 日)。同比(Year-Over-Year)必须对齐「同月同日」,但 2 月 29 日在平年不存在,MongoDB 原生不支持智能日期回退。所以不能靠简单算术偏移,得先标准化成年份无关的「月-日」标识,再关联上年数据。
- 错误做法:
{$add: ["$date", -365*24*60*60000]}→ 平年多算 1 天,闰年少算 1 天,且忽略时区和夏令时 - 正确思路:用
$dateToString提取%m-%d,再用$concat拼上年份,最后用$dateFromString转回日期(失败时需兜底) - 注意:
$dateFromString遇到非法日期(如 2023-02-29)默认返回null,必须配合$cond或$ifNull处理
如何用 $lookup 关联同比数据(同一集合自关联)
聚合中做同比本质是把「本年某日」和「去年同月日」两条记录拉到一起计算。不能用 $match 后分组再查,必须用 $lookup 自关联,且要避免全表扫描——关键在 let + pipeline 中构造精确查询条件。
- 先用
$addFields生成两个字段:md($dateToString: {format: "%m-%d", date: "$date"})和year($year: "$date") -
$lookup的let传入{md: "$md", year: {$subtract: ["$year", 1]}},pipeline 内用$expr匹配$dateToString结果和$year - 务必加
$limit: 1—— 同月同日理论上只有一条,避免数组膨胀;若业务允许同日多条,再考虑$reduce聚合 - 性能提示:确保
date字段有索引;$lookuppipeline 中的$dateToString无法走索引,所以依赖外层已筛选的时间范围(比如先$match近 2 年数据)
环比(Month-over-Month)比同比更麻烦?关键在月份边界处理
环比难点不在「上月」怎么算,而在「上月同星期几」或「上月同日」的取舍。业务常用的是「上月同日」,但 3 月 31 日没有 2 月 31 日,此时应取 2 月最后一天。MongoDB 没有 lastDayOfMonth,得手动算:
- 方案一(推荐):用
$dateFromParts构造下月第 1 天,再减 1 天 →{$dateFromParts: {year: "$year", month: {$add: ["$month", 1]}, day: 1}},然后$subtract86400000 毫秒 - 方案二(简洁):用
$switch判断当前日是否大于目标月最大天数(如 2 月取 28/29),但逻辑分支多,难维护 - 别用
$month直接减 1:1 月会变 0,需$cond特殊处理年份进位 - 注意时区:所有日期操作统一用
timezone: "UTC"参数,避免本地时区导致跨日
实际聚合阶段顺序不能乱:$match 必须放最前
同比环比计算涉及大量日期转换和关联,如果不在开头用 $match 限定时间范围,整个集合都会被加载进内存做 $dateToString 和 $lookup,极易触发 errmsg: "Exceeded memory limit for $lookup"。
- 正确顺序:
$match(时间范围,如{date: {$gte: ISODate("2023-01-01"), $lt: ISODate("2024-01-01")}})→$addFields(提取 md/year/month)→$lookup(同比)→$addFields(计算差值、比率) - 比率计算记得用
$divide包一层,避免除零:例如{$divide: [{"$ifNull": ["$lastYear.value", 0]}, {"$cond": [{"$eq": ["$lastYear.value", 0]}, 1, "$lastYear.value"]}]} - 最终输出字段命名要区分清楚:
currentValue、lastYearValue、yearOnYearRate,别用模糊的value或ratio
真正卡住人的从来不是语法,而是日期边界 case 的覆盖程度——比如 2024-02-29 存在,但 2023-02-29 不存在,这个 null 怎么参与后续计算,决定了同比数字是否可信。











