MongoDB带索引count查询过慢:FETCH阶段耗时过高问题排查
这个问题我碰到过不少次,核心原因是你的索引没有覆盖到所有查询条件,导致MongoDB不得不读取大量文档来做校验——这就是FETCH阶段耗时10秒的根源。
为什么会这样?
你的查询用了status的单字段索引,所以IXSCAN能快速过滤出status="complete"的文档(60ms很正常)。但后面的$or里三个关于groups的条件:
groups字段不存在groups是长度为0的数组groups包含指定ID
这些条件没办法仅通过status的单字段索引来判断,MongoDB必须把每个符合status条件的文档从磁盘读出来,逐一检查groups是否满足要求。62k条文档的磁盘IO开销,直接拖慢了整个查询。
而且你虽然只需要计数,但MongoDB在这种场景下,还是得确认每个文档真的符合所有条件,所以绕不开读取文档的步骤——除非你能让索引覆盖所有判断逻辑。
最有效的优化:创建复合覆盖索引
针对你的查询,创建一个包含status和groups的复合索引:
db.jobs.createIndex({status: 1, groups: 1})
这个索引会把status="complete"的文档对应的groups值(包括字段不存在的情况)都存在索引里。这样MongoDB完全可以在索引层面完成所有条件的判断,根本不需要去读取实际文档,查询速度会直接降到毫秒级。
你可以用explain("executionStats")验证,优化后的执行计划里应该看不到耗时的FETCH阶段,直接由IXSCAN完成计数。
辅助优化:简化查询条件
你可以把groups的条件稍微简化一下,让查询规划器更高效:
原条件里的{"groups":{"$size":0}}其实等价于{"groups": []},所以可以和$in的条件合并,调整后的查询如下:
db.jobs.countDocuments({ "status":"complete", "$or":[ {"groups":{"$exists":false}}, {"groups":{"$in":[[], "5e65ffc2a1e6ef0007bc5fa8"]}} ] })
注意:$exists:false是字段不存在,和字段存在但为空数组是两种不同的情况,不能合并,所以还是要保留在$or里。不过这样的写法能让索引的利用更顺畅。
版本兼容建议:用countDocuments()代替count()
如果你的MongoDB版本是3.6及以上,建议改用countDocuments()而不是count()。count()在某些场景下会返回估算值,而countDocuments()会执行精确计数,同时它的查询规划器在处理复合索引时会更智能。
内容的提问来源于stack exchange,提问作者Alex

