$facet不是并行计算而是单次输入多路分发,价值在于避免重复读取数据;它不加速执行、不支持索引、内存硬限100mb且不可突破,需外层$match剪枝并合理设计子管道结构。

$facet不是并行计算,是单次输入多路分发
它不会让 CPU 或 I/O 并行化,也不会加速子管道内部执行——它的价值在于避免重复读取同一份数据。比如你要同时统计「按城市分组的订单数」和「按日期分组的销售额」,不用写两个聚合、查两次集合,$facet 把它们压进一次扫描里。
常见错误是把它当“并发加速器”用:加了 $facet 后发现响应没变快,甚至更慢。那大概率是因为子管道没剪枝,或者 $facet 放在了 $match 前面,导致全表扫描。
- 必须把高选择性
$match(如{status: "paid", createdAt: {$gte: ...}})放在$facet外层,才能走索引 -
$facet内部的子管道无法触发索引,只接收上游传下来的文档流 - 如果
$facet是管道第一个阶段,MongoDB 会强制COLLSCAN,毫无例外
$facet子管道内存超限(ExceededMemoryLimit)怎么破
这是线上最常炸的坑:$facet 每个子管道的中间结果受 100 MB 内存硬限,且 allowDiskUse 对它完全无效。报错不是 BSONObjTooLarge,而是明确的 ExceededMemoryLimit。
典型触发场景:子管道里用了 $unwind 展开大数组(比如一个文档含 5000 个标签),或 $group 后没 $limit 就直接 $sortByCount。
- 别指望加内存解决——100 MB 是硬编码上限,改不了
- 必须前置剪枝:外层
$match过滤掉无关文档,或子管道内加$limit控制输出条数(如{$limit: 100}) - 对数值字段做
$bucket前,先用{$match: {price: {$exists: true, $type: "number"}}}排除空值,避免跳过导致后续逻辑错乱
$facet输出结构怎么安全提取和组合
$facet 输出是一个对象,每个子管道结果存在独立字段下(如 {"byGender": [...], "byAge": [...]}),彼此隔离。你不能在一个子管道里引用另一个的输出。
想进一步处理,得靠后续阶段显式提取。比如要算“男性占比”,不能在 byGender 子管道里直接除,得等 $facet 结束后用 $project + $arrayElemAt 拿出数组再算。
- 子管道输出始终是数组,哪怕只有一条结果(如
$count的结果是[{"count": 123}]) - 要用
$arrayElemAt提取时,记得检查数组长度,否则$arrayElemAt遇空数组会返回null - 多个子管道结果字段名必须唯一,重复会导致后写覆盖
什么时候不该用$facet
它不是万能开关。如果只需要一个维度的统计,硬套 $facet 反而增加内存开销和理解成本。
真正适合的场景是:需要在同一数据切片上,同步产出多个正交维度的摘要(比如电商后台的「销量分布」「用户地域分布」「复购率趋势」),且这些维度之间无依赖关系。
- 子管道之间有数据依赖(比如第二个要基于第一个的 topN 结果)→ 改用链式管道,别塞进
$facet - 某子管道结果要实时喂给应用做分页 → 考虑用
$facet+$skip/$limit,但注意最终文档不能超 16MB BSON 限制 - 数据量小、查询简单 → 直接拆成多个独立聚合,代码更直白,调试更方便
真正难的从来不是写出语法正确的 $facet,而是预估每个子管道在真实数据量下的内存水位——开发机跑得通,不代表上线后不崩。











