MongoDB $group+$sum聚合查询走全表扫描极慢问题排查
从你贴的explain结果可以直接定位核心原因:查询优化器根本没有生成走status索引的候选执行计划,直接选择了全表扫描(COLLSCAN),证据就是返回结果里rejectedPlans字段为空——不是评估后认为索引效率低,是压根没考虑用索引。
这个是MongoDB 5.0版本聚合优化器的固有行为限制:对于不带任何前置$match过滤条件、直接以$group开头的聚合管道,优化器默认不会评估「覆盖索引扫描+分组计数」的执行路径,哪怕分组字段上已经建了可用的单键索引。
8000万条文档的全表扫描需要把整个集合的数据都加载到内存做遍历、投影、分组,IO和计算成本极高,耗时60秒以上是正常表现。
按改造成本从低到高、性能收益从大到小排序:
1. 加hint强制走覆盖索引(改造成本最低)
直接在聚合参数里指定$hint强制使用status字段的索引,不需要修改管道逻辑:
db.document.aggregate([ { $group: { _id: '$status', status: { $sum: 1 } } } ], { hint: {status: 1} // 替换为你实际创建的status索引定义,单键升序索引即填{status:1} })
加hint后重新执行explain,你会看到执行计划从COLLSCAN变为IXSCAN(索引扫描)+ 覆盖投影,不需要回表查询文档内容。8000万数据量下,查询耗时会从60秒以上降到秒级甚至亚秒级——B树索引的体积远小于全表数据,顺序扫描索引的IO成本比扫全表低一个数量级以上。
2. 前置$sort阶段引导优化器自动选索引(无需硬编码hint)
如果不想在代码里写死hint,可以在$group阶段前增加按status排序的$sort阶段:
db.document.aggregate([ { $sort: {status: 1} }, { $group: { _id: '$status', status: { $sum: 1 } } } ])
优化器识别到排序需求后,会主动选择status上的索引来避免内存排序,同时由于索引本身按status有序,$group阶段可以做流式计数,不需要把全量文档加载到内存分组,性能和加hint的方案基本一致。
注意:如果status是低基数字段(比如只有个位数的枚举值),这个方案完全不会触发聚合分组的内存限制,不需要调整
internalDocumentSourceGroupMaxMemoryBytes参数。
3. 十亿级数据量长期方案:预聚合
如果后续集合数据量涨到十亿级,哪怕是覆盖索引扫描,遍历十亿条索引条目依然会有明显耗时。如果需要高频查询这个统计结果,建议落地预聚合方案:
- 新建独立的统计集合,比如
document_status_stats,每个status对应一条文档,存储对应计数 - 写入/更新原集合的status字段时,用原子操作
$inc同步更新统计集合的计数,也可以通过Change Stream监听原集合变更异步更新统计值 - 查询时直接读取统计集合,耗时可以稳定在毫秒级,完全不需要扫描原集合。
- 先确认你创建的status索引不是部分索引、稀疏索引:部分索引带前置过滤条件,无法用于无过滤的全表统计;稀疏索引会漏统计status字段不存在的文档,优化器也不会主动选择。
- 如果是分片集群部署,上述前两个方案都会在每个分片上先完成局部分组,再把少量分组结果汇总到mongos,不会产生跨分片的大量数据传输,性能表现稳定。
内容的提问来源于stack exchange,提问作者Lakatos Gyula

