MongoDB如何利用索引高效统计字段各取值对应的文档数
问题背景
需求是统计指定字段每个不同取值对应的文档总数,示例集合数据如下:
[ {"a": 5}, {"a": 3}, {"a": 5}, {"a": 4} ]
已在字段a上创建单字段索引,期望得到的统计结果为:
[ {5: 2}, {3: 1}, {4: 1} ]
最初使用常规$group聚合实现:
db.table.aggregate([ { $group: { _id: "$a", count: {$sum: 1} } } ])
百万级文档量下查询速度极慢,慢的根本原因是默认$group阶段无法直接利用索引,会全表扫描拉取所有文档再分组,IO和内存开销都很高。
之后尝试在管道开头加$sort阶段强制走索引,希望有序数据能提升分组效率,但查询耗时依然很长:
db.table.aggregate([ { $sort: { a: 1 } }, { $group: { _id: "$a", count: {$sum: 1} } } ])
从B树索引的实现逻辑看,索引本身已经按字段值有序存储了所有条目,直接遍历索引就能完成计数,不需要回表读完整文档,这类统计理论上执行成本应该非常低。
可直接复用的高效方案
方案1:覆盖索引优化$group(兼容MongoDB 4.4+,推荐优先用)
MongoDB 4.4及以上版本已经支持$group直接命中覆盖索引,只要满足两个条件:分组字段是索引前缀、分组阶段不需要读取索引以外的字段。
之前的写法没命中优化,是因为优化器默认会拉取_id等其他字段,需要显式加$project只保留索引字段,必要时可以用hint强制指定索引,避免优化器选错执行计划:
db.table.aggregate([ // 强制只返回索引里的a字段,不需要回表读完整文档 { $project: { _id: 0, a: 1 } }, { $group: { _id: "$a", count: { $sum: 1 } } } ], { hint: "a_1" })
写完可以用explain("executionStats")验证执行效果,正常情况下totalDocsExamined值为0,totalKeysExamined等于索引总条目数,全程只扫索引不读文档,百万级数据通常几百毫秒就能跑完,比原生写法快10倍以上。
方案2:索引元数据直接计数(兼容MongoDB 5.0+,性能最高)
如果你的MongoDB版本在5.0以上,且统计不需要附加额外查询过滤条件,可以直接读取存储引擎维护的索引统计值返回结果,连索引扫描都不需要,毫秒级出结果:
注意:这个方案返回的计数会包含已经标记删除、但还没完成物理清理的文档,存在秒级的统计延迟,适合对实时强一致要求不高的统计场景,性能是所有方案里最好的。
// 直接读取a字段索引的预统计结果 const stat = db.table.aggregate([ { $collStats: { storageStats: {} } } ]).next().storageStats.indexDetails["a_1"] // 按需要格式化输出即可
方案3:索引游标分批计数(兼容所有MongoDB版本)
如果是4.4以下的低版本MongoDB,没法触发$group的覆盖索引优化,可以直接用索引游标分批遍历索引条目计数,内存占用稳定,不会出现大查询卡库的问题:
const countMap = {} // 强制走a字段索引,只返回a字段,不加载完整文档 const cursor = db.table.find({}, { _id: 0, a: 1 }).hint("a_1").batchSize(10000) while (cursor.hasNext()) { const val = cursor.next().a countMap[val] = (countMap[val] || 0) + 1 } // 转换成你需要的输出格式 const result = Object.entries(countMap).map(([k, v]) => ({[k]: v}))
性能参考
- 原生无优化
$group:全表扫文档,百万级数据耗时数秒到数十秒 - 覆盖索引
$group:仅扫索引,百万级数据耗时数百毫秒 - 元数据统计:直接读预计算值,耗时毫秒级
- 游标遍历索引:仅扫索引,内存无压力,百万级数据耗时1秒左右
内容的提问来源于stack exchange,提问作者TGWaffles

