ToolkitX
知识库工具箱

聚合管道

$match, $group, $sort, $project

25min·进阶

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 放哪里最好?

下一节

索引策略

下一节