$facet必须紧接$match后且$match需命中索引,否则副本集全量扫描;子管道内存超限会静默失败;聚合应走主节点并前置字段裁剪与$limit预控。

$facet 必须紧接 $match 后,否则副本集全量扫描雪崩
副本集本身不改变 $facet 的执行逻辑,但会放大前置过滤缺失的后果。主节点执行聚合时若没前置 $match,所有从节点都会同步执行全集合 COLLSCAN——不是“慢一点”,是每个节点都扛着磁盘 I/O 和网络带宽双压跑完一遍。常见错误写法:[$facet, $match],表面语法合法,实际让整个副本集为同一份无效计算重复耗电。
真正起作用的顺序只能是:[$match, $facet]。且 $match 条件必须能命中索引,例如:{status: "completed", createdAt: {$gte: ISODate("2026-01-01")}}。复合索引字段顺序要和 $match 中键顺序一致,否则索引失效。
- 时间范围 + 状态组合是最稳妥的前置过滤模式
- 避免在
$facet后再加$match—— 子管道已生成,过滤无效 - 副本集选举期间,未完成的聚合可能被中断重试,加剧延迟抖动
$facet 子管道内存超限在副本集上更难察觉
每个 $facet 子管道有严格 100 MB 内存硬限,allowDiskUse: true 对它完全无效。副本集环境下,这个限制不会报错到客户端,而是由主节点静默 kill 掉子管道,返回 ExceededMemoryLimit 错误——但日志分散在各节点,监控指标(如 executionTimeMillis)只显示“某次聚合耗时突增”,不提示具体阶段崩溃。
典型诱因:
-
$unwind大数组(如 tags 字段含 5000 个元素),单文档直接炸成数千行 - 子管道内没加
$limit或$match预剪枝,比如统计前 10 名却先$group全量分组 - 用
$bucket时字段类型不统一,隐式转换导致中间结果膨胀
副本集读写分离配置会误导 $facet 性能预期
很多团队把聚合请求发到从节点以分担主节点压力,但 $facet 不支持从节点读取优化:它依赖上游完整文档流,而从节点的数据可能滞后(尤其 oplog 延迟高时),导致统计结果陈旧;更关键的是,从节点同样要加载全部匹配文档进内存,无法跳过处理——所谓“读从库减负”对 $facet 是假象。
正确做法:
- 聚合一律走主节点,确保数据新鲜性和执行一致性
- 用
$project在$facet前就剔除无关字段(如删掉description、content这类大文本) - 不要指望通过增加从节点数量来横向扩展
$facet吞吐——瓶颈在单节点内存,不是并发数
真正难调的不是语法,是预估子管道水位
开发环境用 1 万条测试数据跑通的 $facet,上线后第二天凌晨因数据量翻倍触发内存超限,这种问题不会出现在本地日志里。副本集各节点内存使用率监控也只显示“整体可用内存下降”,不会标记哪次聚合占了 98 MB。
必须做两件事:
- 在子管道开头强制加
$limit(哪怕只是临时设为 1000,验证逻辑后再逐步放开) - 用
explain("executionStats")查看每个子管道的nReturned和totalDocsExamined,确认是否远超预期
线上聚合挂掉,八成不是代码写错了,而是没人提前算过那几个子管道加起来到底要吃多少内存。











