$facet 不能加速执行但可避免重复读取,需前置$match才能用索引;各子管道内存限100mb且allowdiskuse无效;输出受16mb单文档限制,解析时需手动取result[0]各字段。

$facet 不能加速执行,但能避免重复读取;它适合在一次扫描中同时做分页 + 多维统计,前提是 $match 必须放在 $facet 前面,且各子管道不能超 100MB 内存。
为什么 $facet 放在 $match 后面才能用上索引
因为 $facet 本身不触发索引——它只接收上游传下来的文档流。如果把它写在管道开头,MongoDB 只能全表扫描(COLLSCAN),哪怕你后面加了 $match 也无济于事。
正确顺序必须是:$match → $facet → 其他阶段。比如查「已支付订单」的分页列表和城市分布:
[{"$match": {"status": "paid", "createdAt": {"$gte": ISODate("2026-01-01")}}}, {"$facet": {"data": [{"$sort": {"createdAt": -1}}, {"$skip": 0}, {"$limit": 20}], "count": [{"$count": "total"}], "byCity": [{"$group": {"_id": "$city", "count": {"$sum": 1}}}]}}]
- 前置
$match过滤掉 95% 文档后,$facet的三个子管道才各处理几百或几千条,而不是几百万条 - 时间范围 + 状态组合(如
{status: "paid", createdAt: {$gte: ...}})是最有效的过滤模式 - 如果把
$match放进某个子管道里(比如只在data里加),那count和byCity仍要处理全量数据
$facet 子管道内存 100MB 是硬限制,allowDiskUse 无效
每个子管道(data、count、byCity)各自独立计内存,且严格限制 100MB,超出就报 ExceededMemoryLimit。这个限制无法绕过,allowDiskUse: true 对 $facet 完全不起作用。
-
count子管道基本不会超限,但byCity如果分组字段基数高(比如按用户 ID 分组),或data里用了$unwind膨胀大量文档,就容易崩 - 避免在子管道里做
$unwind+$group组合,尤其当数组平均长度 > 100 时 - 如果需要统计嵌套结构,优先用
$size、$sum等轻量聚合,而不是展开再分组
分页 + 总数 + 多维统计的典型结构怎么写
一个可用的最小可行结构包含三块:分页数据(data)、总数(count)、其他维度统计(如 byStatus)。注意字段名必须是字符串键,不能带空格或特殊字符。
{"$facet": {"data": [{"$sort": {"_id": 1}}, {"$skip": 0}, {"$limit": 10}], "count": [{"$count": "total"}], "byStatus": [{"$group": {"_id": "$status", "count": {"$sum": 1}}}]}}
-
data子管道里必须有$sort,否则$skip/$limit行为不可预测(尤其涉及分片集群) -
count用$count比$group+$sum更省内存,也更快 - 如果某维度统计结果可能为空(比如没找到任何
"shipped"订单),byStatus会返回空数组,业务层需兼容
容易被忽略的边界问题:16MB 单文档限制与结果解析
整个 $facet 输出是一个文档,受 MongoDB 单文档 16MB 上限约束。虽然分页本身控制了 data 大小,但如果你在某个子管道里做了 $push 或 $addToSet 并累积大量原始文档,很容易撞到这个上限。
- 不要在
data子管道里用$project把所有字段都保留,只投影必要字段(比如{"_id": 1, "name": 1, "amount": 1}) - 后端解析时,
$facet返回的是一个对象,不是数组:result[0].data是分页数组,result[0].count[0].total是总数,result[0].byStatus是分组结果数组 - Go/Java 驱动默认不支持自动解包
$facet结果,需手动取result[0]再按 key 读取,别直接当成游标遍历











