不能用多个$group串接做多粒度统计,因为每个$group是不可逆的数据压缩操作,会将原始文档流“压扁”为分组结果,导致后续$group无法访问原始数据;必须用$facet让多套统计逻辑共享同一份输入并行执行。

为什么不能用多个$group串在一起做多粒度统计
因为$group会把输入文档“压扁”成一组聚合结果,后续阶段只能处理这些分组后的文档,原始数据流就断了。比如先按天$group,再想按周统计,就得重新查一遍集合——这等于重复扫描,I/O 和 CPU 都翻倍。
常见错误现象:$group后接另一个$group,发现第二层分组结果为空或字段丢失;或者误以为$unwind能还原原始数组,其实它只展开当前文档里的数组字段,无法回溯到未分组前的完整文档集。
- 每个
$group都是不可逆的数据压缩操作 - 想同时看“日活数”“周活数”“设备类型分布”,必须让三套逻辑共享同一份原始输入
- 硬拼多个独立聚合请求,前端要发 3 次 API,后端要开 3 个 cursor,连接池压力陡增
$facet 是唯一能并发执行多套统计逻辑的聚合阶段
$facet不是“加个功能”,而是设计来解决这个特定问题的:它把一份输入文档同时喂给多个子 pipeline,彼此隔离、并行执行,最后合成一个输出文档。按天、按周、按状态分类,全在一次数据库往返里完成。
正确写法示例(按天/周/月统计订单数):
{ $facet: {
"by_day": [
{ $project: { date: { $dateTrunc: { date: "$created_at", unit: "day", timezone: "Asia/Shanghai" } } } },
{ $group: { _id: "$date", count: { $sum: 1 } } }
],
"by_week": [
{ $project: { date: { $dateTrunc: { date: "$created_at", unit: "week", timezone: "Asia/Shanghai", startOfWeek: "monday" } } } },
{ $group: { _id: "$date", count: { $sum: 1 } } }
],
"by_status": [
{ $group: { _id: "$status", count: { $sum: 1 } } }
]
} }
- 所有子 pipeline 共享同一份
$created_at字段,无需重复$match过滤 - 必须用
$dateTrunc而非$year+$month,否则跨时区或跨年周会错乱 - 字段名(如
by_day)是最终输出的 key,前端直接取result.by_day
容易踩的三个硬限制坑
$facet看着灵活,但 MongoDB 对它的资源控制非常严格,稍不注意就报错。
- 每个子 pipeline 输出的数组总大小不能超过 100 MB ——
allowDiskUse: true对它无效,溢出直接失败 - 整个输出文档不能超过 16 MiB BSON 限制,大结果集建议加
$limit或拆分维度 - 如果
$facet是管道第一个阶段,它会触发COLLSCAN;务必把$match、$sort放在它前面,让索引生效
典型报错信息:errmsg: "Exceeded memory limit for $facet, but didn't allow external sort. Pass allowDiskUse:true to opt in." —— 这条提示是误导的,allowDiskUse 真的没用。
什么时候该放弃 $facet,改用预聚合
当你的“多种统计需求”里包含高频、低延迟、强一致性的场景,比如实时仪表盘每秒刷新「当前在线用户数」「今日下单量」,$facet 就不合适了。
原因很实在:
- 每次调用都要重算,数据量一上 1000 万,哪怕加了索引,
$facet也扛不住每秒几次的并发 - 它没法规避从节点延迟,读取副本时统计值可能滞后几秒
- 真正稳定的方案是写时更新:订单创建时原子
$inc到stats.today_orders字段,查时直接$project取值
复杂点在于时间维度字段要配 TTL 索引自动清理,还有兜底对账脚本——这些都不是 $facet 能绕过去的。


















