CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
MongoDB 入门/课时/聚合管道
RawBlame
课时目录12 节

MongoDB 入门

  1. 01MongoDB 是什么
  2. 02安装与 mongosh
  3. 03文档与 BSON
  4. 04CRUD 操作
  5. 05查询与投影
  6. 06索引
  7. 07聚合管道
  8. 08Spring Data MongoDB
  9. 09何时用 Mongo vs SQL
  10. 10Docker 运行 MongoDB
  11. 11Node.js 驱动与 Mongoose
  12. 12综合练习

聚合管道

$match/$group/$project 入门。

35 分钟

聚合管道

这一课你将能做到

  1. 用 $match → $group → $project → $sort → $limit 写出课时热度 Top 5 统计
  2. 使用 $unwind 展开数组、$lookup 关联集合,并说明 $lookup 与 SQL LEFT JOIN 的对应与代价
  3. 判断什么需求该用 aggregate、什么需求留在 find,并能说出至少两条管道优化原则

为什么要学

列表查询用 find 就够;一旦需求变成「按课统计浏览量、去重用户数、算平均分、排 Top N」,把原始数据拖回应用层拼装既慢又费带宽。聚合管道(Aggregation Pipeline)让这些计算在数据库里一次完成。

这是 MongoDB 面试的重头戏:管道 stage 顺序、$group 语义、$lookup 性能、大数据量优化(allowDiskUse、预聚合),都是高频题。本站场景现成——做题日志的热度榜、每门课的完成率统计。

人话:aggregate = 在数据库里把「过滤、分组、整形、排序」按流水线一次做完,应用层只接收算好的结果。

先建立图像

lesson_logs 文档流
   → $match    (WHERE:尽早过滤,减少后续数据量)
   → $group    (GROUP BY + 聚合函数:$sum/$avg/$addToSet…)
   → $project  (SELECT:改字段名、算派生值)
   → $sort     (ORDER BY)
   → $limit    (LIMIT)
   → 结果文档数组
Stage作用SQL 近似
$match过滤WHERE
$group分组聚合GROUP BY
$project字段整形/派生SELECT 表达式
$sort / $limit / $skip排序截取ORDER BY / LIMIT
$unwind数组展开成多文档行转多行
$lookup关联另一集合LEFT JOIN

跟着做

步骤 1:准备日志数据

use coderoadmap_lab
db.lesson_logs.deleteMany({})

db.lesson_logs.insertMany([
  { lessonSlug: "mongo-crud",     userId: "u_1", action: "view",     at: ISODate("2026-02-01T10:00:00Z") },
  { lessonSlug: "mongo-crud",     userId: "u_1", action: "view",     at: ISODate("2026-02-01T11:00:00Z") },
  { lessonSlug: "mongo-crud",     userId: "u_2", action: "complete", at: ISODate("2026-02-02T09:00:00Z") },
  { lessonSlug: "mongo-intro",    userId: "u_1", action: "view",     at: ISODate("2026-02-01T12:00:00Z") },
  { lessonSlug: "mongo-intro",    userId: "u_3", action: "view",     at: ISODate("2026-02-03T08:00:00Z") },
  { lessonSlug: "mongo-indexes",  userId: "u_2", action: "view",     at: ISODate("2026-02-04T08:00:00Z") }
])

步骤 2:跑通热度榜管道

db.lesson_logs.aggregate([
  { $match: { at: { $gte: ISODate("2026-01-01") } } },   // ① 尽早过滤
  {
    $group: {
      _id: "$lessonSlug",                 // 分组键
      views: { $sum: 1 },                 // 计数
      users: { $addToSet: "$userId" }     // 组内去重集合
    }
  },
  {
    $project: {
      lessonSlug: "$_id",                 // 把分组键改回业务名
      views: 1,
      uniqueUsers: { $size: "$users" },
      _id: 0
    }
  },
  { $sort: { views: -1 } },
  { $limit: 10 }
])

应看到:mongo-crud views 最高;uniqueUsers 是去重人数。注意 $group 之后原文档字段只剩 _id 与你声明的累加器——所以需要 $project 改名。

步骤 3:对照 SQL,写进笔记

SELECT lesson_slug,
       COUNT(*)                       AS views,
       COUNT(DISTINCT user_id)        AS unique_users
FROM lesson_logs
WHERE at >= '2026-01-01'
GROUP BY lesson_slug
ORDER BY views DESC
LIMIT 10;

关键对应:$match=WHERE、$group+$sum=GROUP BY+COUNT、$addToSet+$size=COUNT(DISTINCT)。stage 顺序对应 SQL 语义执行顺序。

步骤 4:$unwind 展开数组

// 每个标签变成一行,才能按标签统计课程数
db.tracks.aggregate([
  { $unwind: "$tags" },
  { $group: { _id: "$tags", courseCount: { $sum: 1 } } },
  { $sort: { courseCount: -1 } }
])

应看到:java、mongodb 等各占一行。$unwind 是「数组统计」的必经之路。

步骤 5:$lookup 关联(理解 + 克制)

db.lesson_logs.aggregate([
  { $match: { action: "view" } },
  { $limit: 2 },
  {
    $lookup: {
      from: "tracks",            // 被关联集合
      localField: "lessonSlug",  // 本集合字段
      foreignField: "slug",      // 对面集合字段
      as: "trackDocs"            // 结果数组字段
    }
  }
])

应看到:每条日志多了 trackDocs 数组(LEFT JOIN 语义:对不上就是空数组)。笔记写下工程态度:能 JOIN 不等于该常 JOIN——文档模型的优势在减少连接,建模上优先内嵌或冗余只读字段,$lookup 留给低频报表。

步骤 6:加时间维度 + allowDiskUse

db.lesson_logs.aggregate([
  {
    $group: {
      _id: {
        day: { $dateToString: { format: "%Y-%m-%d", date: "$at" } },
        lesson: "$lessonSlug"
      },
      views: { $sum: 1 }
    }
  },
  { $sort: { "_id.day": 1, views: -1 } }
], { allowDiskUse: true })   // 数据量大、内存超限时允许落盘

应看到:按「日期 + 课时」二维分组的日活统计。allowDiskUse 是大数据量管道的保险丝。

讲清楚要点

  1. 管道按数组顺序执行:$match/$limit 尽量靠前,让最少的文档流进昂贵的 $group/$sort——这是管道优化第一原则。
  2. $group 的 _id 是分组键:可以是字段("$lessonSlug")、表达式或多字段对象;分组后原文档「消失」,只剩 _id 与累加器字段。
  3. 常用累加器:$sum 计数求和、$avg 平均、$max/$min、$addToSet 去重集合、$push 保留列表——对应 SQL 的聚合函数家族。
  4. $project 负责整形与派生:改名、算比值、$size 取数组长度;让返回结构直接可给前端。
  5. $lookup 是 LEFT JOIN:对不上的文档得到空数组;大集合对大集合的 $lookup 成本高,能内嵌/冗余就别连。
  6. 索引在聚合里依然有效:$match、$sort 能吃索引;聚合前先做索引课的功课。
  7. 超限与优化:$group/$sort 默认受 100 MB 内存限制,超了要么 allowDiskUse、要么拆步预聚合;高频报表考虑定时任务写结果集合($merge/$out)。

你可能会懵的地方

  • $group 之后原来的字段去哪了? 没了。分组把 N 篇文档折叠成 1 篇,只保留 _id(分组键)与显式声明的累加器字段。要保留的维度放进 _id 对象,或先想清楚要不要分组。

  • $match 放在 $group 后面行不行? 语法上行,性能上通常错——那等于先聚合全量再过滤。与 SQL「WHERE 在 GROUP BY 前」同理;确实要过滤聚合结果的(如 HAVING 语义)才放后面。

  • 聚合慢怎么办? 顺序:① $match 提前;② $match/$sort 字段有索引;③ 加 $limit 截断;④ 还不行用 explain 看聚合计划,或改为定时预聚合。

  • 和 MapReduce 什么关系? MapReduce 是旧 API,性能差,现代 MongoDB 一律推荐聚合管道。面试提一句「已不推荐 MapReduce」即可。

  • 什么时候不用聚合? 单篇详情(findOne)、简单列表(find + sort + limit)都不必上管道;管道解决的是「分组统计、多步变换、服务端整形」。

错法对照

错法问题正解
先 $group 再 $match 过滤明细白白聚合无关文档$match 放最前(除非 HAVING 语义)
$group 后直接取 lessonSlug 字段undefined——字段已折叠$project 从 _id 取出改名
一切关联都 $lookup回到大表 JOIN 痛点内嵌 / 冗余只读字段优先
应用层 for 循环 + 多次 find 拼统计慢、错、费带宽能下推的统计交给 aggregate
无 $limit 的大结果集内存与网络双爆加 $limit / 分页 / 预聚合
忘了 allowDiskUse 报 100MB 超限管道中断优化 stage 或显式允许落盘

卡点急救

  1. 现象:$group 结果里拿不到某字段 它没进分组键也没进累加器。把维度放进 _id: { a: "$a", b: "$b" },或对明细值用 $push/$first 保留。
  2. 现象:统计数字和预期对不上 两步核对:① $match 条件是否漏了(如忘了过滤 action);② $addToSet 去重的是不是你要的键。用 countDocuments 抽查对账。
  3. 现象:$sort 阶段超内存报错 $sort 放到 $group 之前先缩小量级,或给排序字段建索引,或 allowDiskUse: true;三者按代价从低到高尝试。

面试常问

  • 介绍聚合管道的执行模型与优化原则。 文档依次流过 stage 数组,每个 stage 输出作为下一个输入。优化原则:$match/$limit 提前减少数据量;$match 与 $sort 利用索引;避免无界 $group/$sort(内存 100MB 限制,超限需 allowDiskUse);高频报表用 $merge/$out 预聚合落集合。

  • $lookup 和 SQL JOIN 的关系?性能上要注意什么? 语义近似 LEFT JOIN(as 字段是数组,对不上为空数组)。性能注意:大集合 × 大集合代价高,逐文档到对面集合取数;应优先靠建模(内嵌、冗余快照字段)减少 lookup,仅低频报表使用;对面字段要建索引。

  • 统计每门课的独立用户数,怎么写? $match 过滤 action → $group by lessonSlug 配 $addToSet: "$userId" → $project 里 $size 取长度 → $sort/$limit。本质是 COUNT(DISTINCT) 的管道表达;海量去重可提 $addToSet 内存隐患,进阶方案 $group + 分片或预聚合。

自测三问

  1. 为什么 $match 应尽量放在 $group 之前?
  2. $addToSet + $size 组合计算的是什么?对应 SQL 的什么?
  3. 什么情况下该避免依赖 $lookup?替代方案是什么?

小结一句话

aggregate 用 stage 管道把过滤、分组、整形、排序在库内一次做完:$match/$group/$project 是入门三件套,$match 提前、$lookup 克制、超限靠 allowDiskUse。

延伸

  • Aggregation 官方文档
  • 聚合管道 Stage 参考
  • 下一课:在 Spring Boot 里用 Spring Data MongoDB 落地这些操作。
加载本节练习…
← 上一课索引下一课 →Spring Data MongoDB