大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
综合练习
这一课你将能做到
- 独立设计并落地
lesson_logs与comments两个集合:文档形状、索引策略、批量数据 - 用聚合产出课时热度 Top 5,用
explain证明常用查询无COLLSCAN,并用countDocuments对账 - 完整陈述「
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、双写为何禁止
讲清楚要点
- 综合题考串联:缺索引或建模随意,会在 explain 与对账环节露馅——每一步都按前面课的清单执行。
- 日志类集合的默认姿势:独立集合 + 时间复合索引 + TTL 过期;绝不内嵌进课程主文档。
- 评论类集合的默认姿势:独立集合 + 作者快照 + 轨道/时间复合索引;文档按「一评论一文档」拆,天然可分页。
- 索引跟随查询建,不是拍脑袋:本课两条复合索引分别服务「按课看时间线」与「按用户看时间线」两个真实接口。
- 聚合结果必须对账:
countDocuments抽查是培养「不盲信输出」习惯的最小动作。 - 可选 API 只是外壳:先用 mongosh 保证数据与查询正确,再套框架——避免在错误数据上堆代码。
- 总结必须写边界:能讲清「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」 | 明确权威源与适用场景 |
卡点急救
- 现象:explain 显示 IXSCAN 但 totalDocsExamined 很大
索引存在但选择性差或排序字段未纳入索引。核对查询条件与 ESR 顺序;给 sort 字段并入复合索引(
{ userId: 1, at: -1 }而非只有{ userId: 1 })。 - 现象:TTL 索引建了但文档没消失
TTL 后台任务每 60 秒跑一次,且按
at字段的实际类型(必须是 Date)计算过期;字符串日期不会被清理。核对字段类型与expireAfterSeconds数值。 - 现象:插入循环报日期非法
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 而是真做过决策。
自测三问
lesson_logs为什么需要两条复合索引而不是一条?- 如何证明某次
find走了索引、且索引是「用对了」? - 用两句话说明本站 MySQL 与 MongoDB 的分工。
小结一句话
做题日志 + 课程评论是 MongoDB 的最佳练手场:建模、复合索引、TTL、聚合与对账一条龙走通;MySQL 依旧掌管权威数据——恭喜完成本 track。
延伸
- Aggregation 官方示例
- Create Indexes 索引创建
- 下一步方向:副本集与高可用(故障转移、读写偏好、多文档事务前提),再到分片集群与 Atlas 托管;建议先在本机用 Docker 起一个单节点副本集,体验事务与选举。