01. 聚合管道是什么
聚合管道(Aggregation Pipeline)是 MongoDB 最强大的数据分析工具。它把数据处理分成多个阶段,每个阶段对上一步的输出做一次加工,像工厂流水线一样——原材料进去,经过一道道工序,最终出来成品。
每个阶段是一个操作符:美元 match 筛选、美元 group 分组、美元 sort 排序、美元 project 投影/计算、美元 limit 限制、美元 skip 跳过。管道里可以有几十个阶段,但建议控制在必要范围内。
聚合管道跟 find 的区别:find 只能做简单的筛选排序,聚合管道能做复杂的数据转换、计算、分组、甚至多集合关联。相当于 SQL 里的 GROUP BY、子查询、窗口函数的综合体。
javascript
// 聚合管道基本结构
db.orders.aggregate([
{ $match: { status: "completed" } }, // 过滤
{ $group: { _id: "$user_id", total: { $sum: "$amount" } } }, // 分组
{ $sort: { total: -1 } }, // 排序
{ $limit: 10 } // 取前 10
]);02. 核心阶段详解——match / group / project
美元 match——相当于 find 的查询条件,放在管道最前面可以减少后面阶段的数据量,提高效率。能用索引的建议前面先 match。
美元 group——分组聚合,相当于 SQL 的 GROUP BY。下划线 id 是分组字段(美元符号开头表示引用字段),其他字段是各种累加器:美元 sum 求和、美元 avg 平均、美元 min/美元 max 极值、美元 push 把值推入数组、美元 addToSet 推入数组但去重、美元 first/美元 last 取第一/最后一个。
美元 project——投影和字段计算,可以保留/排除字段,也能创建计算字段。1 表示保留,0 表示排除。还可以用美元 add、美元 subtract 等表达式计算新字段。
javascript
// $match
db.orders.aggregate([
{ $match: { created_at: { $gte: ISODate("2024-01-01") } } }
]);
// $group
db.orders.aggregate([
{ $group: {
_id: "$user_id",
total_spent: { $sum: "$amount" },
avg_order: { $avg: "$amount" },
order_count: { $sum: 1 }
}}
]);
// $project
db.users.aggregate([
{ $project: {
name: 1,
birthYear: { $subtract: [2024, "$age"] },
_id: 0
}}
]);美元 match 放在管道最前面能利用索引,减少进入后续阶段的数据量。这是性能优化的基本技巧。
03. 数组操作——unwind / lookup
美元 unwind——把数组字段拆开,数组里每个元素变成独立的一行。如果文档里 tags 数组是 ['a','b','c'],美元 unwind 之后变成三行,每行的 tags 字段是单个值。相当于 SQL 的 flatten 展开操作。
美元 lookup——跨集合关联查询,相当于 SQL 的 LEFT OUTER JOIN。在 3.2 版本之前 MongoDB 不支持关联,现在也支持了。from 指定关联的集合,localField 是当前集合的字段,foreignField 是关联集合的字段,as 是结果存到哪个字段。
美元 lookup 有性能开销,建议在关联字段上建索引。非必要不要用 lookup,能在文档设计时用嵌套解决的优先嵌套。
javascript
// $unwind
db.users.aggregate([
{ $unwind: "$hobbies" },
{ $group: { _id: "$hobbies", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
// $lookup
db.orders.aggregate([
{ $lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" }
]);美元 unwind 之后的文档数量可能很大——一个文档里 1000 个元素的数组会炸成 1000 行。注意内存和性能。
04. 管道表达式与条件逻辑
聚合管道里有很多表达式可以做数据转换和条件判断:
美元 add / 美元 subtract / 美元 multiply / 美元 divide——四则运算
美元 concat / 美元 substr / 美元 toUpper / 美元 toLower——字符串操作
美元 cond——三元表达式,相当于 if-else。接收一个条件、为真时的值、为假时的值
美元 switch——多条件分支,相当于 switch-case
美元 ifNull——如果字段为 null 就用默认值
美元 dateToString——日期格式化
这些表达式可以嵌套使用,但嵌套太深可读性会变差,建议复杂逻辑拆成多个阶段。
javascript
// $cond——条件判断
db.users.aggregate([
{ $project: {
name: 1,
level: {
$cond: {
if: { $gte: ["$age", 50] },
then: "senior",
else: "junior"
}
}
}}
]);
// 字符串操作
db.users.aggregate([
{ $project: {
fullName: { $concat: ["$first_name", " ", "$last_name"] },
email: { $toLower: "$email" }
}}
]);美元 cond 可以嵌套,实现多条件判断。但如果条件太多,改用美元 switch 更清晰。
05. 聚合性能优化
聚合管道可能处理百万级数据,优化很关键:
1. 美元 match 放最前面——过滤掉大部分数据,后面阶段压力小。
2. 索引配合——美元 match、美元 sort 用的字段如果有索引,性能提升巨大。
3. allowDiskUse——默认聚合操作内存限制 100MB,超了就报错。加 {allowDiskUse: true} 允许使用磁盘,慢但不会报错。
4. 避免不必要的美元 unwind——一个文档展开成几千行可能把内存撑爆。
5. 美元 limit 和美元 skip 放合适的位置——limit 尽早放减少数据量;skip 放前面,后面阶段不需要跳过的行就不用处理。
6. 用 explain 分析管道——跟 find 的 explain 一样,看看每个阶段处理了多少文档、花了多少时间。
javascript
// 启用磁盘使用
db.orders.aggregate([...], { allowDiskUse: true });
// 查看聚合执行计划
db.orders.explain("executionStats").aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$user_id", total: { $sum: "$amount" } } }
]);allowDiskUse 会让聚合变慢很多(磁盘比内存慢几百倍)。属于不得已才用的方案,能优化 pipeline 就优化。
知识测验
第 1/5 题正确 0
聚合管道里美元 match 放哪里最好?
下一节
下一节 索引策略