应使用 $dateToString 格式化 "%G-W%V" 或 "%Y-%m" 提取年周/年月再分组,避免直接用 $week 导致跨年错乱;需确保日期字段为 Date 类型,必要时用 $dateFromString 转换,并注意时区校正与索引优化。

用 $dateToString 提取年周/年月再分组,别直接用 $week
直接用 $week 会跨年错乱——比如 2024-12-30 是第 53 周,但 2025-01-01 也是第 1 周,两者在分组时被强行拆开。正确做法是组合 $year 和 $week,或更稳妥地用 $dateToString 格式化为 "%G-W%V"(ISO 周)或 "%Y-%m"(年月)。
实操建议:
-
"%G-W%V"对应 ISO 年+周(如"2024-W52"),能保证跨年连续性;"%Y-%m"更直观,适合月度统计 - 字段必须是
Date类型,如果是字符串,先用$dateFromString转换,否则$dateToString返回空 - 聚合阶段顺序不能错:先
$addFields或$project提取周期字段,再$group
聚合管道里怎么写周统计的完整 stage?
以订单表 orders 按 ISO 周统计总金额为例,关键在 $group 前构造可分组的周期字段:
[
{
"$addFields": {
"week": { "$dateToString": { "format": "%G-W%V", "date": "$createdAt" } }
}
},
{
"$group": {
"_id": "$week",
"total": { "$sum": "$amount" },
"count": { "$sum": 1 }
}
},
{ "$sort": { "_id": 1 } }
]
注意点:
-
$createdAt必须存在且为 Date 类型;若字段名是order_time,记得替换 - 结果中
_id是字符串(如"2024-W52"),排序按字典序即自然时间序 - 如果需要补全缺失周(如某周无订单),MongoDB 本身不支持“左连接周序列”,得靠应用层补或用 $range + $map 预生成,代价高,通常不推荐
月统计为什么有时少数据?检查时区和日期边界
用 "%Y-%m" 看似简单,但默认按 UTC 时间截取。如果你的 createdAt 存的是本地时间(比如东八区),而没指定时区,$dateToString 会当成 UTC 解析,导致 2024-03-01T00:00:00+08:00 被转成 UTC 的 2024-02-29T16:00:00,最终归入 "2024-02"。
解决方法:
- 统一存时间戳为 UTC,查询时用
timezone参数校正:{ "$dateToString": { "format": "%Y-%m", "date": "$createdAt", "timezone": "Asia/Shanghai" } } - 或者入库前就转成带时区的 ISO 字符串(如
"2024-03-01T00:00:00+08:00"),MongoDB 4.0+ 能正确解析 - 验证方式:单独查一条记录,用
$dateToString输出原始时间和格式化后值,比对是否偏移
性能问题:没建索引时 $dateToString 会让聚合变慢
$dateToString 是计算型操作,无法利用索引。如果集合大(千万级+)、又频繁按周/月聚合,响应可能从毫秒升到秒级。
优化路径:
- 在写入时就预计算并存储
week_str/month_str字段(如"2024-W52"),然后对这个字段建索引:db.orders.createIndex({ "week_str": 1 }) - 配合 TTL 索引清理过期数据,避免冗余字段膨胀
- 如果业务允许近实时,用 change stream + 预聚合集合(如
orders_weekly)替代每次跑全量聚合
真正上线前,用 explain("executionStats") 看 totalDocsExamined 和 executionTimeMillis —— 如果前者接近集合总数,说明没走索引,得调整。

















