首页/数据/NoSQL 速查/聚合 $facet 一次多面统计
MongoDB99 条

聚合 $facet 一次多面统计

$facet 在同一批输入文档上并行跑多个子管道,一次请求返回多种维度的统计结果,适合仪表盘数据接口。

NoSQL 速查数据更新于 2026-10-13

$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 前移。

易错点

  • $facet 之前缺少 $match:输入集过大时内存与耗时同时放大,还容易直接触发 100MB 阶段限制
  • 在子管道里重复做同一件重活(如均为未收敛的全量扫描):三块结果等于把同一份扫描做了三遍
  • 把 $facet 塞在管道最前面:既拿不到索引裁剪的好处,也让后续阶段无从优化
  • 子管道结果顺序当成稳定:$group 输出顺序不保证,需要顺序时必须在子管道内显式 $sort

数据来源与参考

  • MongoDB Manual: $facet
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌