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

索引

单字段、复合索引、explain。

30 分钟

索引

这一课你将能做到

  1. 创建单字段、复合与唯一索引,并用 getIndexes() 核对、用 dropIndex() 清理
  2. 用 explain("executionStats") 区分 COLLSCAN 与 IXSCAN,读懂 totalDocsExamined 等关键指标
  3. 按 ESR(Equality → Sort → Range)原则为典型查询设计复合索引字段顺序,并解释最左前缀规则

为什么要学

没有索引的过滤在数据变大后会退化成全集合扫描:CPU、磁盘 IO、接口延迟一起崩。索引是数据库面试的顶流考点——MySQL 考 B+ 树最左前缀,MongoDB 考 ESR 与多键索引,底层思想相通。

MongoDB 索引在概念上贴近 MySQL(都是 B 树家族、都有复合索引前缀规则),但多出了多键索引(直接索引数组元素)——这是文档模型的独有优势。本课的目标:会建、会 explain、会按 ESR 排字段顺序。

人话:索引 = 额外维护的一本「按字段排好序的目录」。用一点写入开销与磁盘空间,换查询少翻全表。

先建立图像

查询:{ userId: "u_1001", at: { $gte: … } }  sort: { at: -1 }

ESR 复合索引设计:
  E(Equality)  userId   ← 等值字段放最前
  S(Sort)      at: -1   ← 排序字段其次
  R(Range)     (本例 at 兼任范围)
  → { userId: 1, at: -1 }

explain 关键字段速查:

字段含义
stage: COLLSCAN全集合扫描,大集合上的危险信号
stage: IXSCAN走了索引
totalDocsExamined实际检查的文档数,越小越好
totalKeysExamined扫过的索引键数
nReturned返回条数(理想:≈ totalDocsExamined)
executionTimeMillis耗时参考

跟着做

步骤 1:准备可观测的数据量

use coderoadmap_lab
db.lesson_logs.deleteMany({})

// 批量造 200 条:5 门课 × 10 个用户 × 2 种行为 × 28 天
for (let i = 0; i < 200; i++) {
  db.lesson_logs.insertOne({
    lessonSlug: i % 5 === 0 ? "mongo-indexes" : "mongo-crud",
    userId: "u_" + (i % 10),
    action: i % 2 === 0 ? "view" : "complete",
    at: new Date(Date.UTC(2026, 0, 1 + (i % 28)))
  })
}
db.lesson_logs.countDocuments()   // 200

步骤 2:无索引时 explain,记下基线

db.lesson_logs.find({ userId: "u_1" })
  .sort({ at: -1 })
  .limit(20)
  .explain("executionStats")

在输出里找 winningPlan.stage 与 executionStats:此时应是 COLLSCAN,totalDocsExamined ≈ 200。把两个数抄进笔记——这是「加索引前」基线。

步骤 3:建索引后再 explain

// 单字段
db.lesson_logs.createIndex({ lessonSlug: 1 })

// 复合:等值在前 + 时间排序(ESR)
db.lesson_logs.createIndex({ userId: 1, at: -1 })

// 唯一索引(模拟本站 cf_track.slug 的唯一约束习惯)
db.tracks.createIndex({ slug: 1 }, { unique: true })

db.lesson_logs.getIndexes()   // 应看到 _id_、lessonSlug_1、userId_1_at_-1

重跑步骤 2 的 explain:应变为 IXSCAN,totalDocsExamined 掉到 20 量级(接近 limit)。对照笔记里的基线,感受差距。

步骤 4:故意建一个「字段顺序错误」的索引对比

db.lesson_logs.createIndex(
  { at: -1, userId: 1 },          // 时间在前——与 ESR 相反
  { name: "bad_order_demo" }
)

db.lesson_logs.find(
  { userId: "u_1", at: { $gte: ISODate("2026-01-01") } }
).sort({ at: -1 }).explain("executionStats")

对照两次 explain:bad_order_demo 这条因为首字段不是等值字段,效率明显差。结论写进笔记:有索引 ≠ 索引用对了。

步骤 5:验证最左前缀规则

// 复合索引 { userId: 1, at: -1 },只按第二个字段查:
db.lesson_logs.find({ at: { $gte: ISODate("2026-01-01") } })
  .explain("executionStats")
// winningPlan 里看不到用 userId_1_at_-1 —— 缺少左前缀 userId,用不上

清理演示索引:

db.lesson_logs.dropIndex("bad_order_demo")

保留 { userId: 1, at: -1 } 与 lessonSlug_1 供综合练习使用。

步骤 6:体会多键索引(数组字段的独门能力)

db.tracks.createIndex({ tags: 1 })   // 数组字段也能建——多键索引
db.tracks.find({ tags: "java" }).explain("executionStats")
// stage 变成 IXSCAN;MySQL 对 JSON 数组可没有这种一等公民待遇

讲清楚要点

  1. 索引加速读、拖慢写、占空间:每次 insert/update 都要同步维护所有索引——「每列都建」是反模式,只为真实查询与排序建。
  2. 复合索引有最左前缀规则:{ userId: 1, at: -1 } 能服务「userId 等值」或「userId + at」的查询,但只按 at 查用不上它。
  3. ESR 是字段顺序黄金法则:等值(Equality)→ 排序(Sort)→ 范围(Range)。记住 { userId: 1, at: -1 } 这个模板,面试直接用。
  4. unique: true 让数据库替你挡重复:配合业务唯一键(slug、手机号),比应用层先查后插更可靠(无并发窗口)。
  5. 多键索引是数组字段标配:tags 这类数组自动升级为多键索引,每个元素一个索引项——数组越长索引越大,别给超长数组建。
  6. explain 是唯一证据:优化前后都要留 executionStats,凭「感觉变快了」不算数。
  7. 覆盖查询是进阶奖励:查询与投影的字段全在索引里时可免回表(totalDocsExamined: 0);先保证 IXSCAN 再追求覆盖。

你可能会懵的地方

  • 索引定义里的 1 / -1 是什么? 索引内该字段的排序方向。单字段查询基本无感;复合索引与查询 sort 方向一致时才能免内存排序(所有方向全部相反也等价)。

  • 为什么建了索引还是 COLLSCAN? 常见四因:查询形状不匹配(缺左前缀、前导通配正则)、对字段做了函数/类型转换、集合太小优化器认为全扫更便宜、投影字段迫使回表反而更贵。用 explain 逐项排除。

  • 索引建得越多越好吗? 不是。写路径要维护每条索引,过多索引显著拖慢 insert/update,还占内存与磁盘。定期用 $indexStats 看使用率,清理没用的。

  • TTL 索引是什么? createIndex({ at: 1 }, { expireAfterSeconds: 2592000 }) 让 30 天前的日志文档自动过期删除——做题日志类数据的清理利器,面试可作加分项。

  • 和 MySQL B+ 树什么关系? WiredTiger 的索引底层就是 B+ 树变体,前缀规则与 MySQL 复合索引一致;差异在多键索引、部分索引(partialFilterExpression)等文档库特有玩法。

错法对照

错法问题正解
不 explain,凭感觉说慢无法定位、无法验证explain("executionStats") 拿证据
复合索引字段顺序随意用不上或低效按 ESR 排:等值在前
给低区分度字段单独建索引选择性差收益低并入复合索引
生产高峰在线对大集合建索引资源压力、主从延迟低峰窗口;4.4+ 默认混合构建已缓解
唯一索引遇到字段缺失/null意外撞唯一键了解 null 与缺失的唯一性语义后再上线
无索引就上生产接口数据量一上来全 COLLSCAN上线前 explain 走查核心查询

卡点急救

  1. 现象:explain 里 totalDocsExamined 依然很大 查询形状与索引不匹配。核对:等值字段是否都在索引里且顺序正确、正则是否前缀锚定、类型是否一致(字符串 vs 数字)。
  2. 现象:加了唯一索引,老数据里已有重复导致创建失败 先用聚合找出重复值清理,再建索引;或建部分索引 partialFilterExpression 只约束新数据(需评估业务语义)。
  3. 现象:内存告警与索引相关 检查索引总量与单条大小,删除 $indexStats 中长期零使用的索引;大集合建索引选低峰期并监控复制延迟。

面试常问

  • MongoDB 索引的底层结构是什么? B+ 树(WiredTiger 实现):非叶节点只放键做路由,叶子节点存键与值指针,天然支持等值查找与范围扫描。复合索引按定义顺序多键排序,遵守最左前缀规则——这一点与 MySQL 一致。

  • 什么是 ESR 原则?举例说明。 复合索引字段顺序:Equality 等值条件放最前,Sort 排序其次,Range 范围最后。例:find({ userId }).sort({ at: -1 }) 应建 { userId: 1, at: -1 }——先定位用户(等值收窄),索引内 at 本身有序可免内存排序。若反过来把 at 放前面,等值字段用不上索引前缀,性能骤降。

  • 索引有哪些类型?各自场景? 单字段、复合、唯一(业务键防重)、多键(数组字段)、TTL(自动过期,日志/会话)、文本(搜索)、部分索引(只索引满足条件的文档)、稀疏。重点讲多键与 TTL——这是 Mongo 相对 MySQL 的特色题。

  • 如何确认一条查询走没走索引? explain("executionStats"):看 winningPlan 里是否 IXSCAN(而非 COLLSCAN),对比 totalKeysExamined、totalDocsExamined 与 nReturned 三者是否接近,理想情况接近说明选择性好。

自测三问

  1. COLLSCAN 与 IXSCAN 各意味着什么?优化目标是什么?
  2. 查询「按 userId 等值 + 按 at 降序」,为什么建 { userId: 1, at: -1 }?
  3. 只按 at 范围查,能否高效使用 { userId: 1, at: -1 }?为什么?

小结一句话

索引用写入与空间换读取:核心查询必须 explain 验证 IXSCAN,复合索引按 ESR 排字段,数组走多键,TTL 管过期。

延伸

  • Indexes 官方文档
  • Explain Results 字段说明
  • 下一课:聚合管道——数据库里直接做统计报表。
加载本节练习…
← 上一课查询与投影下一课 →聚合管道