不能直接用 $week 分组,因其仅返回0–53的周数且不带年份,导致2024-12-30与2025-01-01均可能返回1而混入同一组;必须用 $dateToString 格式化为 "%G-W%V" 提取ISO周字符串(如"2025-W01")再分组,确保跨年连续性和准确性。

直接用 $week 分组会跨年错乱,必须用 $dateToString 格式化为 ISO 周("%G-W%V")再分组。
为什么不能直接用 $week 做周分组
因为 $week 只返回 0–53 的数字,不带年份。2024-12-30 和 2025-01-01 都可能返回 1,导致跨年数据被错误归到同一组;更糟的是,有些年份有第 53 周,有些没有,$week 无法保证连续性。
- 错误示例:
{ $group: { _id: { $week: "$createdAt" } } }—— 2024-W52 和 2025-W01 全混进同一个_id: 1 - 正确做法:必须把年份和周数绑定成字符串,如
"2024-W52" - ISO 周标准(
%G-W%V)自动处理跨年边界,比如 2024-12-30 属于 2025 年第 1 周,格式化后就是"2025-W01"
$dateToString 必须配合 Date 类型字段使用
如果 createdAt 是字符串(如 "2024-09-01T08:30:00"),$dateToString 会静默返回空值,最终 $group 得到一堆 _id: null 的桶。
- 先验证字段类型:
db.orders.findOne({ createdAt: { $exists: true } }, { createdAt: 1 }),看输出里createdAt是ISODate(...)还是字符串 - 如果是字符串,必须前置转换:
{ $addFields: { createdAt: { $dateFromString: { dateString: "$createdAt" } } } } - 时区敏感:若原始时间存的是本地时间(如东八区),且未带时区信息,建议入库前统一转 UTC,或在
$dateToString中显式指定timezone: "Asia/Shanghai"
完整周统计管道的典型结构
按 ISO 周统计订单总金额和单量,需确保阶段顺序严格:过滤 → 类型校正 → 周字段提取 → 分组 → 排序。
-
$match必须放第一阶段,否则全量文档进入后续流程,性能断崖下跌 - 提取周字段推荐用
$addFields而非$project,避免意外丢弃其他字段 - 分组键写成
_id: "$week",结果中_id是字符串,天然支持字典序升序(即时间序) - 示例片段:
[ { "$match": { "createdAt": { "$gte": { "$date": "2026-09-01" }, "$lt": { "$date": "2026-10-01" } } } }, { "$addFields": { "week": { "$dateToString": { "format": "%G-W%V", "date": "$createdAt" } } } }, { "$group": { "_id": "$week", "total": { "$sum": "$amount" }, "count": { "$sum": 1 } } }, { "$sort": { "_id": 1 } } ]
缺失周怎么补?别在聚合里硬刚
MongoDB 聚合本身不提供“补全空周”能力。试图用 $range + $map 生成周序列再左连接,会导致内存暴涨、管道极难维护,实际项目中几乎没人这么干。
- 真正靠谱的做法:应用层拉取聚合结果后,用代码生成完整周序列(如从 startWeek 到 endWeek),再 merge 缺失项并设默认值(
total: 0, count: 0) - 如果报表强依赖连续周(比如折线图),前端或服务层补比数据库层补更可控、更易调试
- 注意:补周逻辑必须和聚合用的 ISO 周规则完全一致,否则日期对不上
最常被忽略的是字段类型和时区——哪怕管道写对了,createdAt 是字符串或没设 timezone,结果就全偏移。跑聚合前,先用单条记录验证 $dateToString 输出是否符合预期。











