MongoDB的聚合管道 (Aggregation Pipeline)?
MongoDB 的 聚合管道 (Aggregation Pipeline) 是 MongoDB 中最强大的功能之一,用于处理数据并返回计算结果。
你可以把它想象成工厂里的流水线:文档(数据)进入管道,经过一个又一个的工序(Stage)处理(如筛选、分组、计算、变形),最后输出你需要的结果。
1. 核心概念
- 管道 (Pipeline): 由多个阶段(Stage)组成的数组。
- 阶段 (Stage): 每个阶段对输入的文档进行某种操作(如过滤、分组),并将结果传递给下一个阶段。
- 表达式 (Expression): 在阶段内部使用的逻辑,用于计算值(如
$sum,$avg,$year等)。
基本语法:
javascript
db.collection.aggregate([
{ <stage1> },
{ <stage2> },
...
])
2. 常用阶段 (Common Stages)
以下是最常用的几个阶段,以及它们对应的 SQL 概念:
| 阶段 (Stage) | 描述 | SQL 类比 |
|---|---|---|
$match |
过滤文档,只输出符合条件的文档。 | WHERE |
$group |
将文档分组,可用于统计(求和、平均值等)。 | GROUP BY |
$project |
修改文档结构(选择字段、重命名字段、增加新字段)。 | SELECT |
$sort |
对文档进行排序。 | ORDER BY |
$limit |
限制输出文档的数量。 | LIMIT |
$skip |
跳过指定数量的文档。 | OFFSET |
$unwind |
将数组字段拆分成多条文档(数组中有几个元素就拆成几条)。 | (无直接类比,类似 JOIN 自身) |
$lookup |
执行左外连接,引入其他集合的数据。 | LEFT JOIN |
3. 实战示例
假设我们有一个 订单集合 (orders),数据结构如下:
json
[
{ "_id": 1, "customer": "Alice", "status": "completed", "total": 100, "items": ["A", "B"] },
{ "_id": 2, "customer": "Bob", "status": "pending", "total": 50, "items": ["C"] },
{ "_id": 3, "customer": "Alice", "status": "completed", "total": 200, "items": ["A"] },
{ "_id": 4, "customer": "Dave", "status": "completed", "total": 80, "items": ["B"] }
]
目标:统计每位客户“已完成”订单的总金额,并按金额从高到低排序。
聚合查询代码:
javascript
db.orders.aggregate([
// 第一步:筛选 ($match)
// 只保留状态为 "completed" 的订单
{
$match: { status: "completed" }
},
// 第二步:分组 ($group)
// 按 customer 分组,计算 total 字段的总和
{
$group: {
_id: "$customer", // 必须有 _id,代表分组依据
totalAmount: { $sum: "$total" }, // 创建新字段 totalAmount,值为 total 的累加
orderCount: { $sum: 1 } // 顺便统计订单数量
}
},
// 第三步:排序 ($sort)
// 按 totalAmount 降序排列 (-1 表示降序,1 表示升序)
{
$sort: { totalAmount: -1 }
},
// 第四步:格式化输出 ($project) - 可选
// 调整输出格式,不显示 _id,改名为 customerName
{
$project: {
_id: 0, // 隐藏默认的 _id
customerName: "$_id", // 将之前的 _id (即客户名) 赋值给 customerName
totalAmount: 1, // 保留 totalAmount
orderCount: 1 // 保留 orderCount
}
}
])
执行流程与结果:
$match后: 排除 Bob (pending),剩下 Alice(100), Alice(200), Dave(80)。$group后:- Alice: totalAmount = 300
- Dave: totalAmount = 80
$sort后: Alice 排在 Dave 前面。$project后 (最终结果):json[ { "totalAmount": 300, "orderCount": 2, "customerName": "Alice" }, { "totalAmount": 80, "orderCount": 1, "customerName": "Dave" } ]
4. 进阶阶段详解
A. $lookup (多表关联)
这是 MongoDB 实现类似 SQL JOIN 的方式。
假设还有一个 products 集合,你想在 orders 里看到商品的详细信息:
javascript
db.orders.aggregate([
{
$lookup: {
from: "products", // 关联的目标集合
localField: "items", // 当前集合(orders)的关联字段
foreignField: "name", // 目标集合(products)的关联字段
as: "productDetails" // 结果存入这个新数组字段
}
}
])
B. $unwind (数组展开)
如果 items 是 ["A", "B"],使用 $unwind 后,这一条文档会变成两条:
items: "A"items: "B"
这在需要对数组内部元素进行过滤或分组时非常有用。
5. 性能优化建议 (Best Practices)
- 尽早过滤 (
$matchfirst):- 始终将
$match和$sort放在管道的最开始。 - 原因:这样可以利用 索引 (Index),并且减少后续阶段需要处理的数据量。
- 始终将
- 只查询需要的字段:
- 如果在
$match之后不需要某些大字段(如大段文本),使用$project或$unset尽早去掉它们,减少内存消耗。
- 如果在
- 内存限制:
- 默认情况下,聚合管道的一个阶段最多使用 100MB 内存。如果超过,会报错。
- 解决方法:使用
{ allowDiskUse: true }选项,允许将临时数据写入磁盘(速度会变慢,但不会报错)。
总结
MongoDB 聚合管道是处理复杂数据分析的核心工具。只要记住 "流水线" 这个比喻,明确每一步输入是什么、输出变成了什么,就能轻松掌握它。