索引基础与 B-Tree
本教程共 50 篇 · 第 40 篇 · 更新于 2026-07-30 · 约 4 分钟阅读
40. 索引基础与 B-Tree
本节目标:理解索引为什么快、MongoDB 索引的结构,并会用 explain() 判断查询是否命中索引。
没有索引时,MongoDB 要逐条扫描集合里每个文档才能找到匹配项。集合一大,查询就慢得没法用。索引(Index)就是为解决这个问题而生的。
40.1 为什么索引能加速查询
索引是一份额外维护的小数据集,只存「字段值 + 文档位置」。它把数据按顺序排好,查的时候不必全表扫,直接「翻目录」定位。
打个比方:一本书没目录,找某个词得逐页翻。有目录后,先查目录再翻到对应页,快得多。索引就是数据库的目录。
Note索引提升读性能,但拖慢写性能。每次插入、更新、删除,相关索引也要同步更新。写多的集合别建太多索引。
40.2 MongoDB 用 B-Tree 组织索引
MongoDB 的索引底层是 B-Tree(平衡树)数据结构。B-Tree 把索引键按顺序排布,查找、范围查询、排序都能高效完成。
因为有序,B-Tree 能很好支持:
- 等值匹配:
{ age: 28 } - 范围查询:
{ age: { $gt: 18 } } - 排序:
sort({ age: 1 })
// 在 age 字段建一个升序索引
test> db.users.createIndex({ age: 1 })
1 表示升序,-1 表示降序。对单字段索引,升序降序差别不大,主要影响排序方向。
40.3 默认就有的 _id 索引
每个集合创建时,MongoDB 都会自动在 _id 上建一个唯一索引(Unique Index)。它保证不会有两条文档 _id 相同。
// 查看集合上的所有索引,含默认的 _id 索引
test> db.users.getIndexes()
Warning
_id索引不能删除,也不能改成非唯一。它是 MongoDB 保证文档唯一性的最后一道防线。
40.4 用 explain() 看查询是否走索引
explain() 能告诉我们查询的执行计划:扫了多少文档、用没用索引。先插入些示例数据:
// 灌入示例用户
test> db.users.insertMany([
{ _id: 1, name: "小明", age: 28, city: "杭州" },
{ _id: 2, name: "小红", age: 33, city: "上海" },
{ _id: 3, name: "小刚", age: 22, city: "杭州" }
])
// 没建 city 索引时,会全集合扫描(COLLSCAN)
test> db.users.find({ city: "杭州" }).explain("queryPlanner")
在 queryPlanner.winningPlan 里,stage 为 COLLSCAN 表示全表扫;建索引后变成 IXSCAN,说明走了索引。
test> db.users.createIndex({ city: 1 })
test> db.users.find({ city: "杭州" }).explain("queryPlanner")
Tip调慢查询时,第一步就是
explain()看stage。看到COLLSCAN基本就要加索引了。
40.5 回表:索引查到后还要取文档
索引通常只存部分字段。当查询需要的字段不在索引里,MongoDB 得拿着索引找到的文档位置,回原集合把完整文档取出来,这叫回表(Fetch)。
索引命中 -> 得到文档位置 -> 回集合取完整文档 -> 返回
如果索引本身就包含了查询要的所有字段,就不需要回表。这种「不用回表」的查询,我们叫覆盖查询(Covered Query),第 46 章细讲。
Note回表有额外开销。高频查询若只需少数几个字段,可考虑让索引顺带覆盖它们,省掉回表。
40.6 小结
索引靠 B-Tree 的有序结构加速查询,代价是拖慢写入。_id 索引默认存在且不能删。用 explain() 看 stage 判断走没走索引,理解回表有助于后续优化覆盖查询。