$sort未走索引导致副本集主节点内存爆满,根源是$match未前置致全量排序;必须将高选择性$match放第一阶段并匹配复合索引字段顺序与方向。

聚合管道里$sort没走索引,副本集照样爆内存
副本集本身不改变排序内存行为——报错根源和单节点完全一致:$sort阶段被迫把所有匹配文档加载进内存排序,超出默认 100MB 限制(MongoDB 6.0 以下)就直接失败。主节点执行聚合、从节点只同步 oplog,所以问题一定出在主节点的查询执行计划上,不是复制机制导致的。
为什么$match放后面或没前置,副本集更易触发Sort limit
副本集常用于读写分离场景,但聚合管道的优化规则不变:只有最开头的 $match 才能下推走索引;后面的 $match、$sort、$group 全部在内存里操作。如果业务代码把 $match 写在 $sort 后面,比如:
db.orders.aggregate([
{ $sort: { createdAt: -1 } },
{ $match: { status: "shipped" } }
])
那 MongoDB 就得先把全表文档按 createdAt 排一遍(可能千万级),再过滤 status —— 主节点内存瞬间打满,报错“Sort exceeded memory limit”。这在副本集里尤其危险,因为从节点不参与计算,压力全压在主节点上。
- 必须把高选择性 $match 放第一阶段,例如 { status: "shipped", shop_id: "abc" }
- 对应复合索引字段顺序要严格匹配:db.orders.createIndex({ status: 1, shop_id: 1, createdAt: -1 })
- 别信“反正有副本”,读请求发到从节点也救不了聚合——aggregate 默认只在主节点执行
allowDiskUse: true 在副本集里可能根本没用
这个选项只控制排序是否允许写临时文件,但它在副本集里生效的前提是:你调用 aggregate() 时显式传了 allowDiskUse: true,且驱动层没屏蔽它。但关键限制在于:
- MongoDB Atlas M0(免费版)副本集集群直接禁用该选项,设了也返回 “did not opt in to external sorting”
- 某些旧版驱动(如 Node.js mongodb@4.x 以下)不自动透传
allowDiskUse到服务端 - 即使开了,$sort 前若没 $project 精简字段,长文本/数组字段仍会把大量无效数据拖进内存,磁盘 IO 可能比内存还慢
验证是否真走索引排序,别只看explain结果
在副本集主节点上执行 explain("executionStats"),重点不是有没有 IXSCAN,而是看整个 pipeline 的 stage 链路:
- 如果
executionStats.executionStages.stage是IXSCAN,但inputStage.stage是SORT,说明索引只用了过滤,排序仍内存做 - 聚合管道里,
$sort阶段的executionStages必须显示stage: "SORT"且inputStage.stage: "IXSCAN",才表示排序由索引提供顺序 - 如果看到
stage: "COLLSCAN"或stage: "SORT"下挂inputStage: { stage: "FETCH" },就是典型没命中索引排序
最容易被忽略的是:索引字段顺序必须和 $sort 键顺序完全一致,包括方向。{ a: 1, b: -1 } 的 $sort,索引写成 { a: 1, b: 1 } 就不算匹配。











