最稳妥的做法是用 $facet 并行计算分组统计和总数,再通过 $project 与 $map 结合 $arrayElemAt 提取总数计算百分比;硬写固定值或强行两次 $group 均不可靠。

直接用 $group 统计总数再除,不是最稳妥的做法——因为聚合阶段无法直接引用上一阶段的全局总数,硬写会出错或结果不准。
为什么不能在 $group 里直接除以总行数
很多人尝试这样写:{"percentage": {"$divide": ["$count", 1000]}},但 1000 是写死的;换成变量又没地方取。MongoDB 聚合管道是流式处理,每个文档独立进入阶段,$group 输出的是分组结果,不带“全集总数”这个上下文。
常见错误现象:
-
$sum分组后想除以总文档数,但没地方放total字段 - 用
$project写"$count / $totalCount"报错:字段totalCount不存在 - 强行用两次
$group(先算总数再 join)性能差、易超内存
正确做法:用 $facet 同时跑两路聚合
$facet 是唯一能在一个聚合中并行计算“分组统计”和“总数”的安全方式。它把管道拆成多个子管道,各自独立执行,最后合并结果。
实操建议:
- 第一路子管道:按分类
$group计数,例如{"_id": "$category", "count": {"$sum": 1}} - 第二路子管道:用
{"_id": null, "total": {"$sum": 1}}算全集总数(_id: null表示不分组) - 再用外层
$project+$map遍历分组结果,每项除以total
示例(Shell):
db.collection.aggregate([
{
"$facet": {
"byCategory": [
{"$group": {"_id": "$category", "count": {"$sum": 1}}}
],
"total": [
{"$group": {"_id": null, "total": {"$sum": 1}}}
]
}
},
{
"$project": {
"result": {
"$map": {
"input": "$byCategory",
"as": "item",
"in": {
"category": "$$item._id",
"count": "$$item.count",
"percentage": {
"$multiply": [
{"$divide": ["$$item.count", {"$arrayElemAt": ["$total.total", 0]}]},
100
]
}
}
}
}
}
}
])Spring Boot 中用 MongoTemplate 实现同逻辑
Java 里不能直接写 $facet 的嵌套结构,得靠 Aggregation.newAggregation() 拼装,且注意 $arrayElemAt 提取 total 的写法容易漏掉索引。
关键点:
-
Aggregation.facet()必须放在第一个阶段,否则报错 - 两个子聚合必须用
Aggregation.newAggregation()单独构造,不能混用Criteria -
$project阶段里用.andExpression()写百分比时,要显式用$arrayElemAt取 total 数组首项 - 别忘了
.as("percentage"),否则字段名是默认的expr_0
简化示意(省略 VO 类定义):
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.facet(
Aggregation.newAggregation(
Aggregation.group("category").count().as("count")
).as("byCategory"),
Aggregation.newAggregation(
Aggregation.group().count().as("total")
).as("total")
),
Aggregation.project()
.andExpression("map(byCategory, function(item) { return { category: item._id, count: item.count, percentage: (item.count / arrayElemAt(total, 0).total) * 100 } })")
.as("result")
);替代方案:两步查(适合小数据或低频场景)
如果集合不大(
- 第一步:
mongoTemplate.count(...)拿总数 - 第二步:
Aggregation.match(...).group("category").count().as("count")拿各分类数量 - Java 层循环 list,用总数算百分比(注意 double 除法、避免整除)
注意坑:
- 两步之间数据可能变更(非事务集合下),导致百分比加起来 ≠ 100%
- 并发高时,第一步的 count 可能被缓存,需确认是否加了
Query.with(...)过滤条件一致性 - 不要在
$project里用"$count / #{totalCount}"这种 SpEL 表达式——mongoTemplate不解析它
真正难的不是写对语法,而是判断该不该用 $facet:数据量大、实时性要求高、不能容忍两步误差时,$facet 是绕不开的;其他情况,两步查反而更稳。


















