直接用 $group + $size + $addToSet 可高效统计字段唯一值数量,如 { $group: { _id: null, unique_count: { $size: { $addToSet: "$status" } } } };内存超限时加 allowDiskUse:true 或改用两层 $group(先按字段分组再计数)更稳妥。

直接用 $group + $addToSet 统计唯一值数量,但别真把所有值存下来
想快速知道 status 字段有多少个不同取值?用 $addToSet 配合 $group 确实能办到,但很多人误以为必须先收集全部值再取 size——这在数据量大时会吃光内存甚至触发 errmsg: "Exceeded memory limit for $group"。
真正轻量的做法是:只让 $addToSet 起“标记作用”,不依赖它返回完整数组。实际只需两步:
- 用
$group按目标字段分组,同时用$addToSet: "$status"收集去重值(注意:这是中间步骤,不是最终目的) - 在外层再套一层
$group,对上一步结果的数组用$sum: { $size: "$unique_statuses" }累加长度
更稳妥的写法是避免嵌套 $group,直接用单层 $group + $addToSet + $size:
[
{
$group: {
_id: null,
unique_count: { $size: { $addToSet: "$status" } }
}
}
]
这个聚合管道能直接返回一个文档:{ "_id": null, "unique_count": 5 }。前提是集合不大于 100MB 内存限制(默认),否则仍可能失败。
当 $size + $addToSet 报错 “Exceeded memory limit” 怎么办
错误信息通常是 errmsg: "Exceeded memory limit for $group, but didn't allow external sort. Pass allowDiskUse:true to opt in." ——这不是数据太多,而是 $addToSet 尝试把所有唯一值暂存在内存里,MongoDB 不允许无约束膨胀。
有两个绕过路径:
- 加
allowDiskUse: true(推荐先试):在db.collection.aggregate(...)调用末尾加上该选项,让 MongoDB 允许写临时文件 - 改用
$group+$first模拟去重计数(适合超大数据集):先按字段分组得到每个唯一值一行,再统计行数
第二种方法更稳,尤其字段基数不高时:
[
{ $group: { _id: "$status" } },
{ $group: { _id: null, unique_count: { $sum: 1 } } }
]
它不存值,只建空分组键,内存占用恒定 O(1),但会多一次全表扫描(性能略低,胜在可靠)。
$addToSet 和 $push 在去重统计中不能混用
有人试过用 $push + 手动去重,或者误写成 $addToSet: 1(少写字段路径),结果要么返回空数组,要么报错 "$addToSet only supports objects and arrays"。
关键点只有三个:
-
$addToSet的表达式必须是字段引用,比如"$status"或"$tags.0",不能是字面量 - 如果字段可能为
null或缺失,$addToSet会把null当作一个有效值计入——需要提前用$cond过滤掉 -
$addToSet对数组元素也去重,但如果你传入的是整个数组(如"$tags"),它把整个数组当一个元素,不是展开去重
例如想统计所有 tags 数组里出现过的 tag 字符串总数,得先 $unwind,再 $group:
[
{ $unwind: "$tags" },
{ $group: { _id: null, unique_tags: { $addToSet: "$tags" } } },
{ $project: { count: { $size: "$unique_tags" } } }
]
聚合结果为 0 却不是没数据?检查字段是否存在或类型是否一致
运行完聚合返回 { unique_count: 0 },常见原因不是逻辑错,而是字段压根不存在、全为 null、或类型混乱(比如部分是字符串,部分是数字)。
MongoDB 的 $addToSet 严格区分类型:1 和 "1" 是两个不同值;null、undefined、缺失字段三者在 $group 中行为也不一样。
- 用
db.collection.findOne({ status: { $exists: true, $ne: null } })确认字段真实存在且非空 - 用
db.collection.aggregate([{ $group: { _id: { $type: "$status" }, count: { $sum: 1 } } }])查看字段类型分布 - 若需忽略大小写或格式差异,得在
$group前加$project标准化,比如normalized_status: { $toLower: "$status" }
最常被忽略的一点:聚合默认不索引字段,如果 status 上没建索引,而你又在大集合上反复跑这个统计,响应会越来越慢——建议对高频统计字段补一个稀疏索引:db.collection.createIndex({ status: 1 }, { sparse: true })。


















