有时候,一个 MongoDB 聚合需要返回不止一个结果。
例如,从同一个 payments 集合中,你可能需要按支付方式统计收入、总收入,以及最近已支付的付款记录。
你可以为每个需求分别编写独立的聚合管道。但 $facet 允许你把它们放在同一个管道里。
它接收相同的输入文档,然后将它们送入更小的子管道。每个子管道返回各自的结果。
这就是 $facet 在仪表盘、报表、筛选和分析页面中如此有用的原因。
MongoDB 将 $facet 描述为"在同一阶段对同一输入文档运行多个聚合管道"的方式。更多信息可查阅官方 MongoDB 文档。

在本例中,我们将使用一个 payments 集合。一个文档可能长这样:
{
amount: 120,
method: "card",
currency: "USD",
status: "paid",
paidAt: "2026-05-20T10:30:00Z"
}
假设你想生成一份关于已支付付款的小型报表。
你需要按支付方式统计的收入、总收入,以及最近的已支付付款记录。
所有这些答案都来自同一个集合。
这正是 $facet 发挥作用的地方。你从一个已筛选的付款集合开始,然后将其拆分为不同的结果。
在 VisuaLeaf 中,这个过程更直观,因为你可以先看到数据,然后逐步构建管道。

现在,我们可以一步步构建聚合。
第一个阶段是 $match。
{
$match: {
status: "paid"
}
}
这只保留已支付的付款记录。
最好在 $facet 之前过滤数据,因为每个分支都会使用相同的干净输入。
在 VisuaLeaf 中,你可以直接在预览中看到效果。经过 $match 阶段后,输出应该只显示 status 为 paid 的文档。

过滤完付款记录后,我们可以添加 $facet。
这就是管道开始分叉的地方。
在此之前,聚合只有一条路径。有了 $facet,同一批已支付的付款记录可以流向几个不同的方向。
在本例中,我在 $facet 内创建了三个部分:byMethod、revenueSummary 和 latestPayments。
它们都从相同的已筛选付款记录出发,只是回答不同的问题。
第一个是 byMethod。
在这里,付款记录按 $method 分组。
这样,我们就不再是逐条查看每笔付款,而是可以看到每种支付方式的表现。
例如,信用卡支付有一个总额,银行转账有另一个总额,PayPal 又有另一个总额。
这个分支还会统计每种支付方式有多少笔付款。

接下来是 revenueSummary。
这是已支付付款的快速汇总。
它给出总收入、付款笔数和平均付款金额。
计算出数值后,$project 保持结果简洁。我们不需要这里的每个内部字段,只需要报表中会显示的那些数字。

最后一个是 latestPayments。
这个分支用于获取最近的记录。
它按 paidAt 对已支付付款排序,最新的排在前面,然后仅保留少量结果。
这对于小型表格非常有用,比如仪表盘中的"最新付款"。

完成各分支后,运行聚合。
结果看起来会与普通的文档列表不同。
你不会只得到付款记录,而是得到一个包含独立部分的结果。
{
byMethod: [...],
revenueSummary: [...],
latestPayments: [...]
}
每个部分来自 $facet 内部的一个分支。
byMethod 显示按支付方式分组的收入。
revenueSummary 显示主要数据。
latestPayments 显示最新的已支付记录。
这就是 $facet 非常适合报表的原因——你可以从一个聚合中准备同一个页面的多个部分。

可视化构建管道后,你可以打开生成的查询代码。
这很有用,因为你可以看到可视化步骤背后确切的 MongoDB 聚合语句。
在本例中,查询长这样:
db.payments.aggregate([
{
$match: {
status: "paid"
}
},
{
$facet: {
byMethod: [
{
$group: {
_id: "$method",
totalPayments: { $sum: 1 },
totalAmount: { $sum: "$amount" }
}
},
{
$sort: {
totalAmount: -1
}
}
],
revenueSummary: [
{
$group: {
_id: null,
totalRevenue: { $sum: "$amount" },
numberOfPayments: { $sum: 1 },
averagePayment: { $avg: "$amount" }
}
},
{
$project: {
_id: 0,
totalRevenue: 1,
numberOfPayments: 1,
averagePayment: { $round: ["$averagePayment", 2] }
}
}
],
latestPayments: [
{
$sort: {
paidAt: -1
}
},
{
$limit: 5
},
{
$project: {
_id: 0,
amount: 1,
method: 1,
currency: 1,
paidAt: 1
}
}
]
}
}
])
这让可视化构建器更值得信赖。你不会被锁定在一个黑盒工作流中。你可以可视化地构建管道,然后在需要时阅读、复制或调整生成的代码。

当多个结果来自同一批经过筛选的数据时,$facet 就很有意义。
在本例中,一切都从已支付的付款记录开始。
从那里,我们得到了支付方式汇总、收入概要和最新付款。
这正是你在仪表盘或报表中经常需要的结构。
并不是每个聚合都需要 $facet。如果你只需要一个结果,普通的管道更简单。
但当同一份数据需要回答几个不同的问题时,$facet 能把逻辑保持在同一个地方。
$facet 第一眼看起来有点奇怪,但这个概念并不难。
你从一组文档开始,然后将该数据拆分为不同的结果。
在本例中,我们从已支付的付款记录开始。然后得到按支付方式的收入、收入概要和最新付款。
这就是 $facet 对报表和仪表盘如此有用的原因——你可以从一个聚合中准备同一个页面的多个部分。
而且,当你用可视化方式构建它时,更容易看清每个分支在做什么。
你也可以在 VisuaLeaf 中亲自尝试,一步步查看管道,而不只是阅读代码。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。