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综合练习

综合练习

课程评论/日志集合设计;可选 Spring 读写 API。

45 分钟

综合练习

这一课你将能做到

  1. 独立设计并落地 lesson_logs 与 comments 两个集合:文档形状、索引策略、批量数据
  2. 用聚合产出课时热度 Top 5,用 explain 证明常用查询无 COLLSCAN,并用 countDocuments 对账
  3. 完整陈述「cf_track 留 MySQL、日志与评论进 Mongo」的选型理由,可选通过 Spring 或 Node 完成读写闭环

为什么要学

前面的课是一个个零件:文档建模、CRUD、查询投影、索引、聚合、Spring/Node 集成、Docker 环境。工程能力体现在把它们串成一条可演示路径:建模 → 写入 → 查询 → 索引验证 → 聚合统计 →(可选)API 读写。

这道综合题也是把本 track 变成简历素材的一步:面试聊到 MongoDB 时,你有亲手跑通的集合设计、索引决策、聚合报表和选型论述——全是能展开讲的实战细节。

人话:打勾式刷课不算会;这一课的验收标准是 mongosh(和可选代码)跑出正确且可验证的结果。

先建立图像

任务一 lesson_logs(做题日志)      任务二 comments(课程评论)
  形状:谁在何时对哪课做了什么        形状:评论 + 作者快照 + mentions[]
  索引:{ lessonSlug, at }           索引:{ trackSlug, createdAt }
        { userId, at }
  聚合:热度 Top5 + 去重用户         查询:最新 10 条(投影瘦身)

可选任务三:Spring POST/GET 或 Node 导入脚本
红线:绝不改 MySQL TrackController 权威路径
集合核心问题成功标准
lesson_logs谁在何时对哪课做了什么≥ 20 条数据;两个复合索引;聚合 Top5 与手工计数一致
comments某课程下的评论流索引就位;投影省流量;能说出为何独立成集合

跟着做

步骤 1:重置实验库命名空间

use coderoadmap_lab
db.lesson_logs.deleteMany({})   // 仅实验库!
db.comments.deleteMany({})
db.lesson_logs.getIndexes()     // 观察一下遗留索引

步骤 2:任务一——建模 + 批量造数 + 索引

文档形状(与 Spring / Node 课的字段契约完全一致):

{
  lessonSlug: "mongo-crud",
  trackSlug: "mongodb-essentials",
  userId: "u_1001",
  action: "view",            // view | complete
  at: ISODate("2026-03-01T10:00:00Z")
}

按查询场景先建索引(回忆 ESR:等值在前、排序在后):

db.lesson_logs.createIndex({ lessonSlug: 1, at: -1 })   // 课维度时间线
db.lesson_logs.createIndex({ userId: 1, at: -1 })       // 用户维度时间线
db.lesson_logs.createIndex(
  { at: 1 },
  { expireAfterSeconds: 7776000 }                       // TTL:90 天过期
)

用循环批量造数(至少 3 个用户、5 门课、两种 action 混合):

const users = ["u_1001", "u_1002", "u_1003"];
const lessons = ["mongo-intro", "mongo-crud", "mongo-queries",
                 "mongo-indexes", "mongo-aggregation"];
const actions = ["view", "complete"];
const batch = [];
for (let i = 0; i < 40; i++) {
  batch.push({
    lessonSlug: lessons[i % lessons.length],
    trackSlug: "mongodb-essentials",
    userId: users[i % users.length],
    action: actions[i % 2],
    at: ISODate("2026-03-0" + ((i % 7) + 1) + "T" +
        String(8 + (i % 12)).padStart(2, "0") + ":00:00Z")
  });
}
db.lesson_logs.insertMany(batch)
db.lesson_logs.countDocuments()   // 40

步骤 3:任务一——验证查询与热度聚合

// 常用查询:某用户最近的做题记录
db.lesson_logs.find({ userId: "u_1001" })
  .sort({ at: -1 }).limit(5)
  .explain("executionStats")
// 验收:winningPlan 出现 IXSCAN(userId_1_at_-1),无 COLLSCAN

// 热度 Top5:浏览量 + 去重用户数
db.lesson_logs.aggregate([
  { $match: { action: "view" } },
  { $group: {
      _id: "$lessonSlug",
      views: { $sum: 1 },
      users: { $addToSet: "$userId" }
  }},
  { $project: {
      lessonSlug: "$_id",
      views: 1,
      uniqueUsers: { $size: "$users" },
      _id: 0
  }},
  { $sort: { views: -1 } },
  { $limit: 5 }
])

对账(不可跳过):取榜首的 lessonSlug,手工核对:

db.lesson_logs.countDocuments({ lessonSlug: "mongo-crud", action: "view" })

两个数字相等才算通过。对照 SQL 版写进笔记:

SELECT lesson_slug, COUNT(*) AS views, COUNT(DISTINCT user_id) AS unique_users
FROM lesson_logs
WHERE action = 'view'
GROUP BY lesson_slug
ORDER BY views DESC
LIMIT 5;

步骤 4:任务二——comments 集合

db.comments.insertMany([
  {
    trackSlug: "mongodb-essentials",
    body: "聚合管道讲得很清楚",
    author: { userId: "u_1002", displayName: "学习者A" },
    mentions: ["u_1001"],
    createdAt: ISODate("2026-03-02T08:00:00Z")
  },
  {
    trackSlug: "mongodb-essentials",
    body: "索引 ESR 终于懂了",
    author: { userId: "u_1003", displayName: "学习者B" },
    mentions: [],
    createdAt: ISODate("2026-03-03T08:00:00Z")
  }
])

db.comments.createIndex({ trackSlug: 1, createdAt: -1 })

// 列表接口:投影瘦身 + 最新在前
db.comments.find(
  { trackSlug: "mongodb-essentials" },
  { body: 1, author: 1, createdAt: 1, _id: 0 }
).sort({ createdAt: -1 }).limit(10)

笔记回答(必须写):为什么不把评论无限 $push 进 track 文档或 MySQL 的 JSON 列?提示从五个角度:16 MB 上限、写冲突、分页、索引、字段演进。

步骤 5(可选):Spring 或 Node 读写闭环

  • Spring:实验包新增 POST/GET /api/lab/logs,注入 LessonLogRepository,URI 指向 coderoadmap_lab;不要动现有 TrackController
  • Node:用 Mongoose 写批量导入 comments 的脚本(复用 Schema 校验)

跨库同步如需联动,只允许「事件 + 最终一致」思路,不做权威双写。

步骤 6:验收清单 + 200 字总结

  • 两集合数据、索引(含 TTL)就绪
  • 常用查询 explain 均为 IXSCAN(数据量足够时)
  • 聚合 Top5 与 countDocuments 手工对账一致
  • 200 字总结:为何 cf_track 留 MySQL、日志与评论进 Mongo、双写为何禁止

讲清楚要点

  1. 综合题考串联:缺索引或建模随意,会在 explain 与对账环节露馅——每一步都按前面课的清单执行。
  2. 日志类集合的默认姿势:独立集合 + 时间复合索引 + TTL 过期;绝不内嵌进课程主文档。
  3. 评论类集合的默认姿势:独立集合 + 作者快照 + 轨道/时间复合索引;文档按「一评论一文档」拆,天然可分页。
  4. 索引跟随查询建,不是拍脑袋:本课两条复合索引分别服务「按课看时间线」与「按用户看时间线」两个真实接口。
  5. 聚合结果必须对账:countDocuments 抽查是培养「不盲信输出」习惯的最小动作。
  6. 可选 API 只是外壳:先用 mongosh 保证数据与查询正确,再套框架——避免在错误数据上堆代码。
  7. 总结必须写边界:能讲清「Mongo 承担什么、不承担什么、为什么」,这个 track 才算真正完成。

你可能会懵的地方

  • 数据量太小,explain 还是 COLLSCAN? 优化器在小集合上可能判断全扫更便宜。把数据加到几百条再比;重点确认索引存在且查询形状匹配,这是可迁移的判断力。
  • 聚合 uniqueUsers 和预期不符? 先查 $match 是否漏过滤 action,再确认 $addToSet 去重的键;对账步骤会立刻暴露差异。
  • 可选 API 连不上库? 顺序排查:mongosh 先 ping → URI 与 authSource 逐字符核对 → 库名是否 coderoadmap_lab → Docker 容器是否在跑。
  • 要不要动 MySQL 的表? 不要。本练习全程在 coderoadmap_lab,MySQL 主库零改动——这正是边界意识的验收。

错法对照

错法问题正解
评论内嵌进单篇 track 文档逼近 16 MB、写冲突、难分页独立 comments 集合
不建索引直接交差数据量一上来必爆按查询场景建复合索引并 explain 验证
改 TrackController 读 Mongo 当权威主路径分裂仅 /api/lab/** 实验路由
聚合结果不核对静默逻辑错误countDocuments 手工对账
只建 { lessonSlug, at } 一条索引用户维度查询回表扫描按两个查询维度各建一条
总结不写选型边界学完仍想「换掉 MySQL」明确权威源与适用场景

卡点急救

  1. 现象:explain 显示 IXSCAN 但 totalDocsExamined 很大 索引存在但选择性差或排序字段未纳入索引。核对查询条件与 ESR 顺序;给 sort 字段并入复合索引({ userId: 1, at: -1 } 而非只有 { userId: 1 })。
  2. 现象:TTL 索引建了但文档没消失 TTL 后台任务每 60 秒跑一次,且按 at 字段的实际类型(必须是 Date)计算过期;字符串日期不会被清理。核对字段类型与 expireAfterSeconds 数值。
  3. 现象:插入循环报日期非法 ISODate("2026-03-0" + …) 拼接出错时会出现非法字符串。改用 new Date(Date.UTC(2026, 2, day, hour)) 由数字构造,避免字符串拼接陷阱。

面试常问

  • 「设计一个做题日志系统」你会怎么做? 按本题路径答:集合形状(lessonSlug/trackSlug/userId/action/at)→ 索引(按课与按用户两条 {等值, at} 复合 + TTL 90 天)→ 查询(用户时间线 find + sort + limit;热度走聚合 $match→$group→$sort→$limit)→ 选型边界(权威进度在 MySQL,日志追加型数据进 Mongo)。有数据形状、有索引决策、有聚合方案、有边界——完整答案。

  • 怎么验证你的 MongoDB 查询是高性能的? 三件套:explain("executionStats") 看 IXSCAN 与 totalDocsExamined/nReturned 比值;聚合结果与 countDocuments 对账验证正确性;上线后 $indexStats 持续观察索引命中率。答出「性能证据 + 正确性对账 + 长期监控」三层即为高分。

  • 这个 track 学完,MongoDB 在你项目里的定位是什么? 文档型扩展存储:做题日志、评论、审计这类形状灵活、追加为主、按聚合出报表的数据;权威业务数据与强一致场景仍在 MySQL,跨库用事件最终一致、可重建、绝不双写。能一口气讲清边界,说明不是背 API 而是真做过决策。

自测三问

  1. lesson_logs 为什么需要两条复合索引而不是一条?
  2. 如何证明某次 find 走了索引、且索引是「用对了」?
  3. 用两句话说明本站 MySQL 与 MongoDB 的分工。

小结一句话

做题日志 + 课程评论是 MongoDB 的最佳练手场:建模、复合索引、TTL、聚合与对账一条龙走通;MySQL 依旧掌管权威数据——恭喜完成本 track。

延伸

  • Aggregation 官方示例
  • Create Indexes 索引创建
  • 下一步方向:副本集与高可用(故障转移、读写偏好、多文档事务前提),再到分片集群与 Atlas 托管;建议先在本机用 Docker 起一个单节点副本集,体验事务与选举。
加载本节练习…
← 上一课Node.js 驱动与 Mongoose