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

MongoDB如何利用索引高效统计字段各取值对应的文档数

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 02:28:03