必须用 $dateToString 格式化时间字段,因其支持 timezone 参数可按本地营业日对齐,避免 $year/$month 分开提取导致的时区与跨年错误;再用 $group 按月聚合销售额、订单量、客单价等关键指标,并显式 $sort 保证顺序,空月份建议应用层补全。

直接用 $dateToString 提取年月,再配合 $group 按月汇总,是统计月度销售趋势最稳的路子——别用 $year/$month 单独提取后拼字符串,时区和跨年边界容易出错。
为什么必须用 $dateToString 格式化时间字段
销售数据里的 orderDate 通常是 ISODate 类型,但不同地区订单可能落在 UTC 时间凌晨,如果用 $year 和 $month 分开取值再组合,会把北京时间 1 月 1 日 00:30 的订单算成前一年 12 月(UTC 时间仍是前一日)。$dateToString 支持 timezone 参数,能真正按本地营业日对齐。
- 正确写法:
{ $dateToString: { format: "%Y-%m", date: "$orderDate", timezone: "Asia/Shanghai" } } - 错误写法:
{ $concat: [{ $toString: { $year: "$orderDate" } }, "-", { $toString: { $month: "$orderDate" } }] }(丢失时区控制,跨月逻辑断裂) - 注意:MongoDB 5.0+ 才支持
timezone,旧版本得先用$add手动偏移毫秒数
$group 阶段要聚合哪些关键指标
月度趋势不是只看总金额,得保留可下钻的原子维度,否则后续无法拆解异常月份原因。
- 必选:
_id: { yearMonth: { $dateToString: { ... } } }(作为分组键) - 销售额:
totalSales: { $sum: "$amount" }(注意字段名是否为amount或price * quantity) - 订单量:
orderCount: { $sum: 1 }(别用$count,它不能和其他累加器混用) - 客单价:
avgOrderValue: { $avg: "$amount" }(前提是每条文档是一笔订单;若是一条商品行,得先$group到订单级再聚合)
如何让结果按时间顺序排列并补全空月份
聚合默认不保证顺序,且不会自动填充没数据的月份(比如 2 月没销售,结果里就直接跳过),这会让前端画折线图断档。
- 排序必须显式加:
{ $sort: { "_id.yearMonth": 1 } } - 补空月得靠应用层或 MongoDB 5.2+ 的
$densify,但要注意:$densify要求输入是连续整数或日期范围,得先把yearMonth转成日期类型(如{ $dateFromString: { dateString: "$_id.yearMonth" } }),再$densify,最后转回字符串——步骤多、易出错,生产环境建议在应用层补全 - 简单替代方案:用
$facet同时查出最大最小月份,在内存里生成完整月份列表,再 left join 聚合结果
真正麻烦的不是写对管道,而是销售数据的时间语义是否清晰——订单创建时间?支付成功时间?发货时间?这三个时间点在同一个聚合里混用,月度趋势图就会失真。确认清楚业务口径,再决定用哪个字段进管道。











