Skip to content

MongoDB Aggregate 核心阶段($match/$project/$group/$sort)及分页详解

你想了解 MongoDB 聚合管道中 $match$project$group$sort 的作用,以及聚合分页的实现方式,下面逐一拆解说明:

一、聚合管道(Aggregate)核心阶段详解

MongoDB 的 aggregate() 是基于管道思想的数据分析工具,每个管道阶段(如 $match$project)会接收上一阶段的输出作为输入,处理后传递给下一阶段,最终得到目标结果。

1. $match:数据筛选(过滤)

作用

类似于普通查询的 find() 条件,用于在聚合管道初期过滤不需要的文档,只保留满足条件的文档进入后续管道阶段。

核心特点

  • 尽可能放在管道最前面:减少后续阶段处理的数据量,提升聚合效率(可利用索引优化查询)
  • 支持与 find() 相同的查询操作符($eq$gt$in$regex 等)

示例

筛选出 age 大于 20 且 gender 为 "male" 的用户文档:

javascript
db.users.aggregate([
  {
    $match: {
      age: { $gt: 20 },
      gender: "male"
    }
  }
])

2. $project:字段投影(筛选/重命名/计算)

作用

控制文档的字段展示形式,支持三种核心操作:

  1. 保留需要的字段、排除不需要的字段(类似 find() 的第二个参数)
  2. 重命名字段
  3. 生成新的计算字段(基于原有字段做运算、拼接等)

核心特点

  • 1 表示保留字段,0 表示排除字段(_id 字段默认保留,需手动指定 _id: 0 排除)
  • 支持通过 $ 引用原有字段,进行算术运算、字符串拼接等

示例

保留 name 字段,排除 _id,重命名 ageuserAge,新增 fullName 字段(拼接 firstNamelastName):

javascript
db.users.aggregate([
  {
    $project: {
      _id: 0,
      name: 1,
      userAge: "$age", // 重命名
      fullName: { $concat: ["$firstName", " ", "$lastName"] } // 新增计算字段
    }
  }
])

3. $group:数据分组聚合

作用

按照指定字段对文档进行分组,并对每组数据进行聚合计算(如统计数量、求和、平均值等),类似 SQL 中的 GROUP BY

核心特点

  • 必须指定 _id 字段:用于定义分组依据(_id: "$gender" 按性别分组,_id: null 表示全局分组(所有文档为一组))
  • 支持多种聚合操作符:$sum(求和)、$avg(平均值)、$count(计数)、$max/$min(最大/最小值)等

示例

gender 字段分组,统计每组用户数量(userCount)和平均年龄(avgAge):

javascript
db.users.aggregate([
  {
    $group: {
      _id: "$gender", // 分组依据:性别
      userCount: { $count: {} }, // 计数(等价于 $sum: 1)
      avgAge: { $avg: "$age" } // 计算平均年龄
    }
  }
])

4. $sort:结果排序

作用

对当前管道阶段的文档进行排序,支持升序或降序排列,类似 SQL 中的 ORDER BY

核心特点

  • 1 表示升序,-1 表示降序
  • 可按单个字段或多个字段排序(多字段排序时,按配置顺序优先级递减)
  • 若在 $skip/$limit(分页阶段)前使用,可实现有序分页

示例

先按 age 降序排序,再按 name 升序排序:

javascript
db.users.aggregate([
  {
    $sort: {
      age: -1, // 年龄降序
      name: 1  // 姓名升序
    }
  }
])

二、MongoDB 聚合分页实现

聚合分页依赖两个专用管道阶段:$skip(跳过指定数量文档)和 $limit(限制返回文档数量),通常与 $sort 配合使用,保证分页结果的一致性。

1. 分页核心阶段说明

管道阶段作用示例
$skip跳过前面 N 条文档,用于指定分页的起始位置$skip: 20(跳过前 20 条,对应第 3 页,每页 10 条)
$limit限制返回的文档数量,用于指定每页显示条数$limit: 10(每页显示 10 条)

2. 分页完整流程(含排序)

核心逻辑

$match(筛选)→ $sort(排序,保证分页有序)→ $skip(跳过前面页数的文档)→ $limit(获取当前页文档)

公式

  • 跳过文档数 = (当前页码 - 1) * 每页显示条数
  • 示例:第 2 页,每页 10 条 → 跳过 (2-1)*10=10

完整示例

查询 gender 为 "female" 的用户,按 age 降序排序,实现第 2 页、每页 10 条的分页:

javascript
// 分页参数
const pageNum = 2; // 当前页码
const pageSize = 10; // 每页条数
const skipCount = (pageNum - 1) * pageSize; // 跳过的文档数

db.users.aggregate([
  // 1. 筛选数据(尽可能放在最前面)
  { $match: { gender: "female" } },
  // 2. 排序(保证分页结果一致,必须在 skip/limit 前)
  { $sort: { age: -1 } },
  // 3. 跳过前面的文档
  { $skip: skipCount },
  // 4. 限制当前页返回条数
  { $limit: pageSize },
  // 可选:投影需要的字段
  { $project: { _id: 0, name: 1, age: 1, gender: 1 } }
])

3. 分页总数查询(可选,用于计算总页数)

若需要显示总页数(如“共 5 页”),需额外执行一次聚合,用 $count 统计符合条件的总文档数:

javascript
// 查询总条数
db.users.aggregate([
  { $match: { gender: "female" } }, // 与分页查询的筛选条件一致
  { $count: "total" } // 统计总文档数,返回 { total: 45 } 格式
])

// 总页数 = Math.ceil(总条数 / 每页条数) → Math.ceil(45 / 10) = 5 页

三、注意事项

  1. 管道顺序影响性能:$match 尽量放在最前,$sort 放在 $skip/$limit 前,避免无效排序/分页
  2. 大数据量分页:$skip 在数据量极大时(如跳过 10 万条以上)性能较差,可优化为“基于最后一条文档的字段值分页”(如按 _id 或时间戳)
  3. 索引优化:为 $match$sort 的字段建立索引,大幅提升聚合效率

总结

  1. $match:筛选文档,减少后续处理量;$project:控制字段展示/生成计算字段
  2. $group:按字段分组并做聚合统计;$sort:对文档进行升序/降序排列
  3. 聚合分页:核心是 $skip(跳过前 N 条)+ $limit(每页条数),配合 $sort 保证有序,总条数用 $count 查询
  4. 分页公式:跳过数 = (当前页码-1) * 每页条数,管道顺序建议:$match$sort$skip$limit