聚合下推失败时,mongos被迫全量拉取数据再计算,关键依据是explain("executionstats")中stage出现在shards还是mongos的executionstages;常见原因包括$match字段非分片键、$sort/$group未匹配分片键前缀、$unwind破坏分布及$facet引发结果合并。

聚合下推失败时,mongos不得不合并
不是 mongos 主动选择“合并”,而是当 pipeline 中某些阶段无法在各 shard 独立执行时,mongos 别无选择——只能把中间结果全拉过来再算。关键判断依据是 explain("executionStats") 输出里,哪些 stage 出现在 shards 字段下,哪些只挂在 mongos 的 executionStages 里。
常见触发全量拉取的场景:
-
$match过滤字段不是分片键,或虽是分片键但用了$ne、$not、$regex(非前缀)等无法下推的操作符 -
$sort排序字段与分片键无关,或排序字段是复合分片键但没按前缀顺序使用(比如分片键是{a: 1, b: 1},却只按b排序) -
$group的_id表达式不等于分片键(如_id: "$shardKey"),或带聚合表达式($sum、$avg)且未配合$group: {_id: "$shardKey"}做预分组 -
$unwind后紧跟$group:展开破坏了原始分片分布,mongos 必须收齐所有文档才能正确归并
为什么 $facet 会在 mongos 合并结果
$facet 的每个子 pipeline 都会在每个 shard 上完整执行一遍,然后 mongos 把所有 shard 返回的各个 facet 分支结果拼起来。这不是“合并计算”,而是“合并结果集”。它本身不卡住计算,但极易引发内存问题——尤其当 facet 分支多、每分支返回大量文档时,mongos 内存可能直接 OOM。
实操建议:
- 避免在
$facet里写$sort或$limit——这些阶段无法下推,会放大传输和内存压力 - 用
allowDiskUse: true只能缓解磁盘问题,不能解决数据拉取量大的本质 - 如果只是想做多维度统计,优先考虑用多个独立聚合代替
$facet,由应用层合并
mongos 合并阶段的实际执行位置
mongos 本身不持久化状态,也不执行磁盘写入;但它确实会执行部分 stage,比如 MERGE_SORT、MERGE_COUNTPROXY。这些 stage 在 explain 输出中明确标记为 mergeType: "mongos",属于正常归并行为。
真正危险的是看到 COLLSCAN 出现在 mongos 的 executionStages 中——这意味着 mongos 正在对全量拉回的数据做全表扫描,说明前面已有 stage 完全没下推。
注意两个例外情况:
- 当
$lookup关联的是未分片集合(即system.views或同库内 unsharded coll),整个 merge 必须落在主分片上,此时mergeType是"primaryShard",不是 mongos - 若 pipeline 含
$group且设置了allowDiskUse: true,mongos 会随机选一个 shard 执行 merge(mergeType: "anyShard"),而非自己扛着
如何验证聚合是否被合理下推
最直接的办法是跑 db.collection.aggregate([...], {explain: "executionStats"}),重点看三块:
-
executionStages.shards:每个 shard 实际跑的 pipeline 片段,应包含尽可能多的$match、$project、$addFields -
totalDocsExamined和totalKeysExamined:如果数值远超单个 shard 的文档总数,说明有 stage 没下推 -
executionStages.stage类型:出现MERGE_SORT是 OK 的;出现COLLSCAN或SORT在 mongos 节点下,就是警报
复杂点在于,即使 $match 下推了,$sort + $limit 组合仍大概率卡在 mongos——因为排序需要全局有序,而分片间无法保证局部有序结果能直接拼出全局有序。这点容易被忽略,也最难优化。











