$facet是单次读取、多路分发的确定性机制,非并行加速器;须置于$match之后以利用索引避免COLLSCAN,且各子管道内存严格限制100MB、allowDiskUse无效,超限即报ExceededMemoryLimit。

$facet 是多维分析的唯一可行入口,但它不是“并行加速器”,而是单次读取、多路分发的确定性机制。 它能避免重复扫描集合,但每个子管道内存上限 100 MB 且 allowDiskUse 无效——线上聚合挂掉,八成是因为这个限制被悄悄突破了。
为什么 $facet 必须放在 $match 后面?
因为 $facet 本身不走索引,它只接收上游传来的文档流。如果把它放在管道最前面,MongoDB 只能全表扫描(COLLSCAN);而把高选择性 $match 放在它前面,就能利用索引快速过滤出目标文档集,再喂给 $facet 的多个子管道。
- 错误写法:
[$facet, $match]→ 强制全量扫描,性能雪崩 - 正确顺序:
[$match, $facet]→ 索引生效,子管道只处理千级/万级文档 - 特别注意:时间范围 + 状态组合(如
{status: "completed", order_date: {$gte: ...}})是最有效的前置过滤模式
如何避免 ExceededMemoryLimit 错误?
这个错误不是来自最终输出大小,而是子管道中间结果超 100 MB 内存硬限。典型诱因是 $unwind 大数组或 $group 后没加 $limit。
- 对含 5000 个标签的
tags字段直接$unwind→ 每个文档炸成 5000 行,内存瞬间爆掉 - 子管道内必须前置剪枝:
{$match: {price: {$exists: true, $type: "number"}}}或{$limit: 100} -
allowDiskUse: true对$facet子管道完全无效,别试
怎么安全地在不同 facet 中用 $bucket 和 $sortByCount?
可以组合,但字段路径和空值处理必须显式对齐,否则统计口径错位。
-
$bucket要求数值型字段,$sortByCount对字符串也有效,但路径要一致(如都用"$category.name") - 缺失
price的文档会被$bucket自动跳过,但$sortByCount默认把null当作一个独立分组 → 结果里多出一条{"_id": null, "count": 123} - 统一加
{$match: {price: {$ne: null}}}或{$match: {category: {$ne: null}}}预清洗 - 子管道之间完全隔离,后续要用
$project+$arrayElemAt显式提取某一路结果
真正难的不是写出语法正确的 $facet,而是预估子管道内存水位。开发环境跑得通的聚合,在生产数据量翻倍后第二天凌晨就可能报 ExceededMemoryLimit——这种问题不会出现在本地日志里,只会在监控告警和业务中断时突然浮现。


















