MongoDB的聚合管道(Aggregation Pipeline)是数据处理的一把利器,它可以把文档像流水线一样依次送入多个处理阶段,每个阶段完成一类操作,最终输出我们想要的结果。很多初学者只知道用find查询数据,遇到分组统计、多表关联这类需求时就束手无策了。这篇文章将从管道的基本原理讲起,逐步拆解常用操作阶段的用法,最后通过一个完整的订单分析案例,带你彻底掌握聚合管道。

一、聚合管道的基本原理
聚合管道的核心思想是“阶段流水线”。一个聚合操作由一个或多个阶段(Stage)组成,文档从第一个阶段进入,处理后传给下一个阶段,像工厂流水线一样层层加工。语法上,聚合使用aggregate方法,参数是一个数组,数组中的每个元素代表一个阶段。
与普通的find查询相比,聚合的能力要强得多。find只能做筛选和简单的字段投影,而聚合可以做分组统计、数组拆解、多集合关联、结果重塑等复杂操作。可以把find理解为“把数据捞出来”,而聚合是“边捞边加工”。当然,如果是简单的条件查询,find的性能更好,没必要动用聚合。
理解管道有一个关键点:阶段的顺序非常重要。比如先$match再$group,是先过滤再分组;反过来先分组再过滤,语义就完全变了,性能也可能差很多。一般来说,能尽早过滤的阶段应该放在前面,这样后续阶段处理的数据量更小,整个管道执行得更快。
二、常用管道阶段详解
MongoDB提供了几十个管道阶段,日常开发中用得最多的是下面这几个,先把它们吃透,大部分统计需求都能应对。
1. $match:数据过滤
$match的语法和find的查询条件完全一样,用于筛选出符合条件的文档。它最大的价值在于提前缩小数据范围,如果能配合索引使用,性能提升非常明显。这也是为什么$match通常被放在管道的最前面。
db.orders.aggregate([
{ $match: { status: "paid", amount: { $gt: 100 } } }
])
2. $group:分组统计
$group是聚合管道的灵魂阶段,它按指定字段分组,并对每组做统计计算。_id字段是分组的依据,写法比较特殊,需要用$字段名的形式引用文档字段。统计结果则通过累加器操作符生成,比如$sum求和、$avg求平均、$max取最大值、$push把值收集成数组。
db.orders.aggregate([
{
$group: {
_id: "$categoryId", // 按商品类目分组
totalAmount: { $sum: "$amount" }, // 销售额求和
orderCount: { $sum: 1 }, // 订单数量
avgAmount: { $avg: "$amount" } // 客单价
}
}
])
3. $project 与 $sort:投影与排序
$project用来挑选输出字段,可以理解为查询中的投影,但它还能重命名字段、构造新字段,甚至做简单的字段运算。$sort则对文档排序,和find中的用法一致。在分组之后接一个$sort,是统计报表类需求的常见组合。
db.orders.aggregate([
{ $group: { _id: "$categoryId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }, // 按销售额降序
{ $project: { _id: 0, categoryId: "$_id", total: 1 } } // 重塑输出结构
])
4. $lookup:关联查询
当数据分散在多个集合中时,$lookup可以实现类似SQL的LEFT JOIN效果。比如订单集合里只存了用户ID,用户名称、等级等信息在用户集合里,就可以用$lookup把两边的数据拼起来。被关联集合上的关联字段最好建索引,否则大数据量下会很慢。
db.orders.aggregate([
{
$lookup: {
from: "users", // 被关联的集合
localField: "userId", // 当前文档的字段
foreignField: "_id", // 目标集合的字段
as: "userInfo" // 输出的数组字段名
}
}
])
三、实战案例:订单销售额统计分析
下面用一个完整的需求把前面讲的阶段串起来。假设有一个订单集合,需求是:统计最近30天各商品类目的销售额,只看金额大于50的已支付订单,输出类目名称、订单数、销售额和客单价,按销售额从高到低排列,只取前10名。
分析一下这个需求,处理顺序应该是:先按时间和状态过滤,再按类目分组统计,然后排序取前几名,最后整理输出字段。写成聚合语句如下:
db.orders.aggregate([
// 第一步:过滤出30天内已支付且金额大于50的订单
{ $match: {
status: "paid",
amount: { $gt: 50 },
createTime: { $gte: new Date(Date.now() - 30 * 24 * 3600 * 1000) }
}},
// 第二步:按类目分组统计
{ $group: {
_id: "$categoryId",
totalAmount: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgAmount: { $avg: "$amount" }
}},
// 第三步:按销售额降序排列
{ $sort: { totalAmount: -1 } },
// 第四步:只取前10名
{ $limit: 10 },
// 第五步:整理输出结构
{ $project: {
_id: 0,
categoryId: "$_id",
totalAmount: 1,
orderCount: 1,
avgAmount: { $round: ["$avgAmount", 2] } // 客单价保留两位小数
}}
])
这条语句展示了管道组合的典型思路:每个阶段只做一件事,层层递进。如果后续要加上类目名称,只需在$match后面插入一个$lookup阶段关联类目集合即可,扩展起来非常灵活。
四、性能优化与常见坑
写聚合容易,写高性能的聚合则需要一些经验。首先要善用索引,$match和$sort如果放在管道开头且字段上有索引,MongoDB可以直接利用索引扫描,避免全集合遍历。所以过滤条件尽量前置,这不仅是语义问题,更是性能问题。
其次要注意内存限制。默认情况下,单个聚合阶段占用内存不能超过100MB,超过就会直接报错。遇到这种情况有两种解决办法:一是给涉及排序的字段建索引,让排序走索引;二是在allowDiskUse参数中允许落盘,让MongoDB把中间结果写到临时文件中。落盘虽然能解决问题,但速度会明显变慢,只建议作为兜底手段。
db.orders.aggregate(
[ /* 管道阶段 */ ],
{ allowDiskUse: true } // 允许使用磁盘临时文件
)
还有一个常见的坑是$group的_id写法。分组字段必须写成$字段名,漏掉美元符号会被当成常量,所有文档会被分到同一组。如果需要按多个字段组合分组,可以把_id写成一个对象,例如_id: { city: "$city", category: "$categoryId" }。另外,在分片集群环境下,包含$lookup或$graphLookup的聚合对数据分布有要求,设计分片键时需要提前考虑。
总的来说,聚合管道是MongoDB从“存储工具”升级为“分析工具”的关键能力。掌握$match、$group、$sort、$project这几个核心阶段,再理解管道顺序和索引的关系,日常开发中百分之九十的统计需求都能轻松搞定。遇到更复杂的场景,再逐步学习$unwind、$facet、$bucket等进阶阶段,你会发现聚合管道几乎能满足所有数据加工需求。
MongoDB聚合管道MongoDB aggregate分组统计修改时间:2026-09-13 07:00:31