$facet 是什么
$facet 让同一批输入文档并行进入多个子管道,一次请求返回多种维度的统计结果。子管道之间互相独立,各自从头处理完全相同的输入集,因此它适合仪表盘那种「一个页面要展示分类计数、Top N、区间分布」的场景,能用一次数据库往返替代多次查询。
基本信息
| 语法 | { $facet: { 名称A: [子管道], 名称B: [子管道] } } |
|---|---|
| 位置要求 | 通常放在管道靠后位置,前面用 $match 收敛数据 |
| 子管道限制 | 子管道内不能再出现 $facet,也不应包含写入类阶段 |
| 返回结构 | 单个文档,各子管道结果作为其字段值(数组) |
| 内存约束 | 每个子管道各自维护中间状态,受单阶段 100MB 内存上限约束 |
典型使用场景
- 仪表盘接口:一次返回状态分布、Top N 商品、每日趋势三块数据
- 搜索结果页:同一条件同时算出分类计数与商品列表
- 分页加总数:用两个子管道分别取当前页数据与总数,避免两次查询
- 区间分布:用 $bucket 配合 $facet 输出价格或时长分桶统计
- 多维度报表:按渠道、按地区、按品类分别聚合,减少应用层拼装
示例
示例
db.orders.aggregate([
// 先用高选择性条件收敛输入,facet 的代价与输入文档量强相关
{ $match: { ts: { $gte: ISODate("2026-10-01T00:00:00Z") }, tenant: "t-1" } },
{ $facet: {
byStatus: [ { $group: { _id: "$status", n: { $sum: 1 } } }, { $sort: { n: -1 } } ],
topSkus: [ { $group: { _id: "$sku", qty: { $sum: "$qty" } } }, { $sort: { qty: -1 } }, { $limit: 5 } ],
total: [ { $count: "n" } ],
page: [ { $sort: { ts: -1 } }, { $skip: 0 }, { $limit: 20 } ]
} }
])常见子管道写法
| 用途 | 子管道写法 | 说明 |
|---|---|---|
| 分组计数 | [ { $group: { _id: "$字段", n: { $sum: 1 } } } ] | 最常用的一块,注意结果再排序 |
| Top N | 分组后接 $sort 与 $limit | 子管道内排序只影响该块结果 |
| 总数 | [ { $count: "n" } ] | 与分页块配合,省掉单独的一次计数查询 |
| 分页块 | $sort 后接 $skip 与 $limit | 深偏移仍然慢,深处翻页建议改用游标 |
| 区间分桶 | [ { $bucket: { groupBy: ..., boundaries: [...], output: {...} } } ] | 用于价格、时长等数值分布 |
子管道各自完整处理同一批输入,块数越多总代价越高:三到四块通常合适,如果某个块能独立成一次查询且有索引支持,单独查询可能更划算。
$facet 之前若没有收敛条件,输入集会被整批缓存进各子管道,大数据量下容易触发内存限制;必要时配合 allowDiskUse,但优先做的是把 $match 前移。