用过 MongoDB 的人都知道 createIndex,也多半背过「加索引能让查询变快」。但这个说法停留在接口层,掩盖了索引真正的本质:MongoDB 的索引不是「字段上的标记」,而是 WiredTiger 里一棵独立的 B-tree。每建一个索引,就是在存储引擎的 B-tree 森林里多栽一棵树。

理解这一层,才能解释几个线上常见的疑问:为什么文档数没涨多少,加了索引后磁盘和内存占用却明显上升?为什么一个集合写得很慢,删掉两个索引就快了?为什么 createIndex 在大集合上要跑很久,还不能立刻中断?这些都是「索引是独立 B-tree」这个事实的延伸。

先把机制边界说清楚

MongoDB 在 WiredTiger 里维护两类 B-tree:

  • 集合 B-tree:按 RecordId(插入顺序的内部 ID)存储完整文档,是数据的真正落点。
  • 索引 B-tree:每个索引对应一棵独立的 B-tree,按索引键排序,条目是「索引键 → RecordId」。

查询走索引时,优化器先在索引 B-tree 里定位(IXSCAN),拿到一批 RecordId,再根据需要回到集合 B-tree 读完整文档(FETCH,俗称回表)。如果查询只需要索引里的字段,连回表都省了,这就是覆盖查询(covered query)。

这套结构和 MySQL InnoDB 的「聚簇索引 + 二级索引」很像,但有个关键差别:MongoDB 的集合 B-tree 按 RecordId 组织,不是按某个聚簇键。这意味着MongoDB 默认没有「按业务主键聚集」的能力(除非用 5.3+ 的 clustered collection),所有二级索引查询都可能回表。

索引在内存里长什么样

MongoDB 索引:WiredTiger 的 B-tree 怎么组织

把索引 B-tree 和集合 B-tree 画在一起,索引的本质立刻清晰:它是一份按某个键排序的、指向文档位置的「目录」。查得快,是因为 B-tree 的 O(log N) 定位;写得慢,是因为每棵索引树都要跟着改。

索引为什么让查询快

索引的快来自 B-tree 的对数定位。一个千万级文档的集合,全表扫描(COLLSCAN)要逐个比较,复杂度是 O(N);走索引(IXSCAN)在 B-tree 里只要几次页跳转就能定位,复杂度是 O(log N)。

这个差距在大集合上非常显著。同样是查 age = 31

  • 没有 age 索引:扫完千万文档,CPU 和 IO 都吃满。
  • age 索引:在索引 B-tree 里几次跳转定位到 age=31 的条目,拿到 RecordId,回集合 B-tree 读那一篇文档。

但「快」是有条件的。索引只在选择性高的查询上明显加速。如果 age 只有「男/女」两个值,索引选择性极差,优化器可能判断走索引比全表扫还慢(要反复回表),干脆选 COLLSCAN。这就是为什么「给每个字段都加索引」是错的——选择性差的索引既占空间又用不上。

索引为什么让写入变慢

这是索引最容易被低估的代价。每写一条文档,WiredTiger 要维护:

  • 集合 B-tree 1 次(写文档本身)。
  • 每个索引 B-tree 1 次(更新所有索引)。
  • oplog 1 次(复制日志)。
  • journal 1 次(崩溃恢复日志)。

所以一个有 5 个索引的集合,一次写要做约 7 次页修改;同样写入量,无索引的集合只要 2 次。写入 QPS 一高,这个 3.5 倍的写放大会直接体现在延迟和 IO 上。

更隐蔽的是索引对 Cache 的占用。WiredTiger 的 Cache 是有限的,索引 B-tree 的页也要常驻内存才能保证查询快。索引越多,工作集越大,Cache 命中率越容易下降,反而拖慢所有查询。

索引的几个反直觉点

createIndex 是后台成本。 在大集合上建索引,WiredTiger 要把所有文档扫一遍、排序、构建整棵 B-tree。这个过程占用 CPU 和 IO,createIndex 默认是前台构建(4.2 起合并为一种方式,但仍是大操作),大集合上可能跑几十分钟。生产环境建索引要错峰,或用滚动方式。

索引不等于一定走。 优化器是按成本选计划,不是「有索引就用」。选择性差、返回大量结果、排序需求不匹配,都可能让优化器放弃索引。用 explain 看实际走了什么,而不是假设。

复合索引有顺序。 {a: 1, b: 1}{b: 1, a: 1} 是两棵不同的 B-tree,能服务的查询完全不同。复合索引的字段顺序决定了它能加速哪些查询——这是下一篇 ESR 原则要展开的核心。

多键索引(数组字段)更贵。 文档里某个字段是数组时,索引会对数组的每个元素建一条目,一个文档可能贡献多条索引项。大数组的字段建索引,索引膨胀会比想象中大很多。

判断框架

把上面的内容收成几条可复用的判断:

  • 索引是独立的 B-tree,不是字段标记。加索引 = 加一棵树 + 加一份写放大。
  • 只给高频查询、选择性高的字段建索引,不要「以防万一」地全字段索引。
  • 写密集的集合要严格控制索引数量,每个索引都要能证明它值回写入成本。
  • 索引建好后用 explain 验证是否被使用,没被用上的索引是纯负债。
  • 大数组字段慎建索引,多键索引的膨胀是隐性内存和存储开销。
  • 任何「加索引没变快」的疑问,先看 executionStatstotalKeysExaminedtotalDocsExamined,而不是怀疑 MongoDB。

索引是 MongoDB 性能治理的主战场,但它的本质是查询和写入之间的一笔交易。下一篇会沿着复合索引展开,讲清楚字段顺序的 ESR 原则。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 MongoDB」,欢迎关注公众号 「十三Tech」。后续会按文档模型、索引优化、存储引擎、高可用和分片集群这条线更新。

十三Tech公众号二维码