You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 16:33:26