$facet 是 mongodb 中唯一支持并行执行多个子管道的阶段,适合同时获取分页列表和总数;$count 会丢弃文档结构,$group 配合 $push 易触发内存限制,而 $facet 将二者隔离处理,避免相互影响。

$facet 是 MongoDB 聚合管道中唯一能“并行执行多个子管道”的阶段,它天然适合同时查列表数据和总数——不需要两次往返、也不依赖客户端计算。
为什么不能用 $count 或 $group 直接替代 $facet?
单独用 $count 会丢弃原始文档结构,无法同时返回分页列表;$group 虽能统计总数,但必须配合 $push 才能保留列表,而大数据量下 $push 易触发内存限制(100MB 默认)或 OOM。$facet 把“查列表”和“查总数”拆成两个隔离子管道,互不影响。
标准写法:一个 $facet 包含 list 和 total 两个子管道
典型结构如下,注意字段名、排序、跳过/限制位置:
[
{
"$facet": {
"list": [
{ "$sort": { "createdAt": -1 } },
{ "$skip": 0 },
{ "$limit": 20 }
],
"total": [
{ "$count": "count" }
]
}
},
{
"$project": {
"data": "$list",
"total": { "$arrayElemAt": ["$total.count", 0] }
}
}
]
-
$facet内部的list子管道负责分页逻辑,$skip/$limit必须放在这里,不能放在 $facet 外面 -
total子管道用$count最简洁;若需带条件的总数(比如只统计 status: "active"),就在$count前加$match -
$project阶段用$arrayElemAt提取$total的第一个元素,因为$count返回的是单元素数组
常见错误:聚合结果为空或 total 为 null
现象:total 字段是 null 或缺失。原因通常是子管道没匹配到任何文档,$count 返回空数组而非 [{"count": 0}]。
- 修复方式:在
total子管道末尾加{$addFields: {count: {$ifNull: ["$count", 0]}}},再用$project提取 - 更稳妥写法:把
$count换成$group——[{$group: {_id: null, count: {$sum: 1}}}, {$project: {count: 1, _id: 0}}],这样无匹配时也会返回{count: 0} - 别忽略索引:如果
list子管道有$match,确保对应字段建了复合索引(如{status: 1, createdAt: -1}),否则$skip/$limit无法高效跳过前 N 条
性能敏感点:大数据量下避免 $facet 成为瓶颈
$facet 会把整个输入集复制两份分别进子管道,如果上游未过滤,可能放大内存压力。
- 务必在 $facet 前加
$match尽早缩小数据集(例如按时间范围、状态过滤) - 不要在 $facet 后再做复杂计算(如
$lookup),应尽量把关联逻辑移到list子管道内部 - 当总数稳定且更新不频繁时,考虑用单独的计数集合(counter collection)缓存
total,$facet 只负责查列表
真正要注意的是子管道的独立性——list 和 total 看似一起写,实则完全隔离,连索引使用、内存分配、错误处理都是分开的。漏掉任一子管道的 $match 或写错字段路径,都只会让那一半失效,很难一眼发现。











