想象你是一座百万册图书馆的唯一管理员。馆里的藏书完全乱序摆放:有的躺在角落,有的藏在最高层,还有的夹在过道里。某天,一位读者匆匆跑来:“我要找一本封面印着红色书签、讲海边小镇的书!”你只好从第一本开始,一本一本地翻封面。运气好十分钟找到,运气不好要翻到夕阳西下。 后来馆长请来一位资深顾问,他为你做了一件事:给每本书制作一张目录卡,上面写着书名、作者、存放位置,并把这些卡片按书名拼音组织进一排档案柜。从此,读者再要什么书,你只需打开柜子,几个来回就翻到对应卡片,再按卡片上的位置去拿书——快得惊人。 MongoDB 就像这座图书馆。它属于 NoSQL 中的“文档型数据库”,每条数据是一份 JSON 文档,文档存放在集合(collection)里。没有索引时,查询要挨个扫描集合里所有文档,这叫“全集合扫描”(COLLSCAN);有了索引,MongoDB 就像拿到了那套目录卡,能够直接定位到目标文档。本文全程用这个“图书馆”的比喻,带你理解 MongoDB 索引的工作原理、常见类型、优化技巧,以及那些让无数人踩坑的“目录卡陷阱”。

岗位关联

对后端工程师、DBA 和 AI 应用开发者而言,掌握 MongoDB 索引是解决慢查询的必修课。绝大多数线上性能问题,最终都指向同一个根源:索引没设计好。学会它,你就能把查询速度从“秒级”提升到“毫秒级”。

目录
  1. 可视化演示
  2. 索引基础:没有目录的图书馆
  3. B-Tree原理:倒着长的目录树
  4. 索引类型:不同样式的目录卡
  5. 索引操作:卡片的增查删
  6. 索引优化:让查询再快一步
  7. 特殊索引:会魔法、会认字的卡
  8. 限制与实战:索引不是魔法
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

索引基础:没有目录的图书馆

图书馆管理员第一次面对十万多本乱序摆放的书时,内心是崩溃的。读者随口报一个书名,他就得从第一排书架摸索到最后一排。直到有一天,他想通了:与其每次现找,不如提前做一套“目录卡”。每张卡记录“书名 + 位置”,按书名排序收进柜子。从此找书变成了“翻卡 → 取书”两步,轻松无比。

在 MongoDB 这个“图书馆”里,集合中的文档默认也是“乱序”存放的。当你执行一个不带索引的查询时,MongoDB 会把集合里每个文档都读一遍,逐一比对字段值,这就是全集合扫描(collection scan)。而索引就是那套目录卡:它把字段值当作“书名”,把文档在磁盘上的存储位置当作“柜子编号”,预先排好序存起来。当查询命中索引字段时,数据库不再需要翻遍所有文档。

创建索引的命令非常简洁:db.collection.createIndex({username: 1})。数字 1 表示升序,-1 表示降序。MongoDB 默认用 B-Tree 来组织索引,让查找、插入、删除都保持高效。有了索引,等值查询、范围查询、排序都能沿着有序结构快速完成。索引的本质是“空间换时间”——用额外的存储和写入开销,换取大幅提升的查询速度。

这就能解释为什么图书管理员都离不开目录卡:有索引,千万本书的查询只是翻几次卡片柜;没索引,就只能一天到晚在书架之间来回跑。速度的差距,不是快一倍两倍,而是几个数量级。

Cscan=O(n),Cindex=O(log⁡n)\displaystyle C_{\text{scan}} = O(n), \quad C_{\text{index}} = O(\log n)
全集合扫描的代价随文档数线性增长,而索引查找的代价只随文档数对数增长。百万级文档时,两者差距达到数万倍。
  1. C_scan 表示全集合扫描的代价没有索引时,查询要遍历集合中所有文档,最坏情况要读 n 次才能找到目标。
    Cscan=O(n)C_{\text{scan}} = O(n)
  2. n 是集合中的文档总数这是决定扫描成本的核心变量,文档越多,扫描越慢。
    n=∣C∣n = |\mathcal{C}|
  3. C_index 表示使用索引后的查找代价B-Tree 索引把查找变成“翻目录”,每次比较都能排除一大半候选。
    Cindex=O(log⁡n)C_{\text{index}} = O(\log n)
  4. log n 的实际含义以 2 为底的对数,100 万文档时 log₂(10⁶) ≈ 20,意思是约 20 次比较就能定位,而全扫描要 100 万次。
    log⁡2106≈20\log_2 10^6 \approx 20

B-Tree原理:倒着长的目录树

图书馆的目录柜并不是一个巨无霸抽屉,而是一棵“倒着长的树”。最顶上有一个总目录抽屉,里面不写每本书的具体信息,只告诉你“某个字母范围去哪一柜”;顺着指示走到下一层,又是更细的范围提示;最后落到一个具体抽屉,里面才是排好序的卡片。

这就是 B-Tree(平衡多路查找树)。MongoDB 的每个索引都构建在这样一棵树之上。根节点相当于“总目录”,内部节点逐层缩小范围,叶子节点才存放真正的“卡片”——每个键值对应的文档位置。和二叉树不同,B-Tree 的每个节点可以拥有多个子节点,这个数量叫“扇出”。扇出越大,树越“胖”,层数越少。

B-Tree 的核心特征是“有序”和“平衡”:同一节点内的键从左到右递增;任何时刻,从根到所有叶子的路径长度保持一致。查找时,从根开始,与节点内的键做比较,决定进入哪个分支,每走一层,候选范围就缩小到原来的 1/m。特别适合范围查找,因为叶子层的键天然有序,$gt、$lt、sort 等操作都能高效完成。

100 万张卡片,如果每个节点能分 100 个分支,树高只有 3 层。管理员从总目录出发,最多往下走 3 层就能抽到想要的那张卡。这比在 100 万个抽屉里逐个翻,省下的功夫可想而知。

h=⌈log⁡mN⌉\displaystyle h = \lceil \log_{m} N \rceil
树高约等于以扇出为底、对索引条目数取对数。树高越低,一次查询需要读取的磁盘页越少,速度越快。
  1. N 是索引条目数即集合中被索引的字段值总数,通常等于文档数。多键索引中数组元素展开后,N 会大于文档数。
    N=索引条目数量N = \text{索引条目数量}
  2. m 是节点扇出一个节点最多能拥有的子节点数量。m 越大,树越“胖”、越矮,查找越少层级。
    m=每个节点的最大出度m = \text{每个节点的最大出度}
  3. h 是树高从根到叶子的层数。因为层数必须是整数,所以用向上取整符号 ⌈⌉。
    h=⌈log⁡mN⌉h = \lceil \log_{m} N \rceil
  4. 磁盘 I/O 次数一次索引查找大概要读取 h 层磁盘页,树高 3 往往意味着 3 次磁盘 I/O 就能定位。
    I/O 次数≈h\text{I/O 次数} \approx h

索引类型:不同样式的目录卡

图书馆的需求越来越复杂:有人按作者找书,有人要找包含某个标签的所有书,还有人担心两本书重名。于是管理员开始制作不同类型的卡片:按作者姓氏排列的卡片、带标签索引的卡片、记录唯一编号的卡片。每种卡片服务一类特定的查书方式。

MongoDB 也提供了丰富的索引类型来应对不同查询模式。最基础的是单字段索引,适合对单个字段做等值或范围查询。复合索引则是多个字段联合排序,例如 {author: 1, title: 1},当查询和排序同时涉及 author、title 时效率极高。多键索引针对数组字段:对 {tags: 1} 建索引时,MongoDB 会自动为数组中的每个元素生成一条索引项。

唯一索引用来保证字段值不重复,比如用户名、订单号,_id 本身就是一个内置的隐式唯一索引。稀疏索引则只对“存在该字段”的文档建索引,适合那些字段只在少数文档中出现的场景。理解这些类型,能为不同的业务查询选对“卡片样式”。

管理员会在卡片上标注“作者”“标签”“唯一编号”,MongoDB 的不同索引类型也各自解决一类查询问题。选错类型,轻则查询变慢,重则出现订单号重复这样的严重事故。

M=∑d∈C∣d.tags∣\displaystyle M = \sum_{d \in \mathcal{C}} |d.\text{tags}|
多键索引的条目总数等于集合中所有文档数组长度之和。数组越长,索引体积越大,写入维护成本越高。
  1. C 是文档的集合代表当前集合中的所有文档。
    C={集合中的全部文档}\mathcal{C} = \{\text{集合中的全部文档}\}
  2. d.tags 是文档中的数组字段|d.tags| 表示该文档数组的元素个数,例如 tags: [“小说”,“悬疑”] 的长度为 2。
    ∣d.tags∣=数组元素数量|d.\text{tags}| = \text{数组元素数量}
  3. M 是多键索引条目总数多键索引要为每个数组元素单独建一条索引记录,M 等于所有文档数组长度的总和。
    M=所有文档数组元素个数之和M = \text{所有文档数组元素个数之和}
  4. 示例假设 100 篇文档平均每篇有 5 个标签,那么多键索引条目数就是 500。
    M=100×5=500M = 100 \times 5 = 500

索引操作:卡片的增查删

目录卡不是从天上掉下来的。管理员要动手做卡,定期检查卡片有没有损坏,不需要时还要果断撤掉旧卡。这一整套“卡片操作”,对应 MongoDB 的索引管理命令。

创建索引用 createIndex,例如 db.collection.createIndex({field: 1}, {name: "idx_field"})。对大集合建索引建议用后台构建,避免阻塞业务读写。也可以附加选项:unique: true 建唯一索引,sparse: true 建稀疏索引,两者还能组合使用。查看当前集合有哪些索引,用 db.collection.getIndexes(),它会返回索引名、键和属性;删除不再需要的索引用 db.collection.dropIndex("idx_field")。

索引方向也要注意,1 表示升序、-1 表示降序。单字段索引正反扫描都可以用同一个索引,但复合索引的方向要根据查询排序模式来选择,否则会出现“内存排序”的性能损耗。好的索引运维,就是及时为业务建卡、查卡、撤卡。

建卡要花时间,查卡要花功夫,撤卡要干净利落。索引管理的本质,就是花得体的成本,建设最优的目录体系。

Sindex=N×(Skey+Sptr)\displaystyle S_{\text{index}} = N \times (S_{\text{key}} + S_{\text{ptr}})
索引占用空间等于条目数乘以每条索引记录(键值 + 指针)的大小。索引不是零成本,它会线性消耗磁盘。
  1. N 是索引条目数一般为文档数,多键索引则为数组元素总数。
    N=索引条目数量N = \text{索引条目数量}
  2. S_key 是键的平均大小被索引字段值的字节数,字符串字段通常比整型字段更占空间。
    Skey=单个键的平均字节数S_{\text{key}} = \text{单个键的平均字节数}
  3. S_ptr 是指针大小每条索引记录还要存指向文档的引用,即文档的存储位置。
    Sptr=指针的平均字节数S_{\text{ptr}} = \text{指针的平均字节数}
  4. S_index 是索引占用总空间由条目数乘上键与指针大小之和得出,呈线性增长。
    Sindex=N×(Skey+Sptr)S_{\text{index}} = N \times (S_{\text{key}} + S_{\text{ptr}})

索引优化:让查询再快一步

管理员渐渐成了“索引专家”。他发现,有些卡片连书的简介都写了,读者不用取书就能得到答案;有些卡片按“姓氏 + 名字”排列,只提供姓氏也能使用;而当某类卡片对应的书特别多时,直接去书架翻反而更快。这些经验,对应 MongoDB 索引优化的几个核心概念。

覆盖索引(covered query)是指查询所需的所有字段都包含在索引中,MongoDB 不必回表读原始文档。比如索引 {name: 1, age: 1},查询只返回 name、age 且条件也只涉及 name,数据直接从索引取,用 explain 能看到 totalDocsExamined: 0。最左前缀原则告诉我们:复合索引 {a: 1, b: 1} 能服务“查 a”和“查 a+b”,但单独查 b 则不行。设计复合索引时,把等值条件放前面,范围排序条件放后面。

索引选择性是另一个关键指标:字段去重值数量除以文档总数。选择性越接近 1,表示每个索引值对应的文档越少,定位越精准;像“性别”这种只有两三个值的字段,选择性极低,索引往往派不上用场。最后,务必养成用 explain() 分析查询计划的习惯,重点看 stage 是 IXSCAN(索引扫描)还是 COLLSCAN(集合扫描)。

优秀的图书管理员不会给“是否在馆”这种二值卡片建厚厚一本目录。他会把卡片花在真正能区分书的关键信息上,并尽量让卡片本身携带足够信息,避免频繁取书。

Sel=NdistinctNtotal\displaystyle Sel = \frac{N_{\text{distinct}}}{N_{\text{total}}}
选择性衡量一个索引字段区分文档的能力。选择性越高,索引越有价值;选择性接近 0 时,走索引甚至比全表扫描更慢。
  1. N_total 是文档总数集合中的全部文档数量。
    Ntotal=集合文档总数N_{\text{total}} = \text{集合文档总数}
  2. N_distinct 是字段去重后的值数量例如“城市”字段在 5 万文档中只有 5 个不同取值,那么去重数就是 5。
    Ndistinct=唯一值数量N_{\text{distinct}} = \text{唯一值数量}
  3. Sel 是选择性去重数除以总数。值越接近 1,每个索引值对应的文档越少,越适合建索引。
    Sel=NdistinctNtotalSel = \frac{N_{\text{distinct}}}{N_{\text{total}}}
  4. 示例5 万文档中城市有 5 个取值,选择性只有 0.0001,这样的索引价值很低。
    Sel=550000=0.0001Sel = \frac{5}{50000} = 0.0001

特殊索引:会魔法、会认字的卡

图书馆里有三类特殊卡片:一类是报纸的“过期定时销毁单”,一类是给书标注关键词的标签,还有一类是地图上的坐标图钉。它们分别对应 MongoDB 的 TTL 索引、文本索引和地理位置索引。

TTL 索引用于自动清理过期数据。创建方式是在时间字段上加 expireAfterSeconds,例如 createIndex({createdAt: 1}, {expireAfterSeconds: 3600})。MongoDB 的后台线程会定期扫描,把超过指定秒数的文档删除。它非常适合日志、会话、验证码这类“只在一段时间内有价值”的数据。文本索引则用于全文检索:createIndex({description: "text"}) 建好之后,用 $text 操作符就能对字符串内容做关键词搜索,适合文章检索、商品描述搜索等场景,中文环境通常还要配合分词工具。

地理位置索引中,2dsphere 用于球面几何查询。对 location 字段建 2dsphere 索引后,可以使用 $near、$geoWithin 等操作符,实现“附近的门店”“区域内设备”这类 LBS 功能。

过期数据交给 TTL 自动回收,关键词交给文本索引扫描,坐标位置交给地理索引计算。有了这些特殊卡片,很多业务需求就不再需要人工遍历大量文档。

texpire=tcreated+ΔtTTL\displaystyle t_{\text{expire}} = t_{\text{created}} + \Delta t_{\text{TTL}}
TTL 索引的本质是为每条文档计算一个过期时刻,当前时间超过该时刻就删除。过期时长由创建索引时的参数决定。
  1. t_created 是文档插入时间TTL 依赖的日期字段值,写入时一般存当前时间 new Date()。
    tcreated=文档时间字段值t_{\text{created}} = \text{文档时间字段值}
  2. Δt_TTL 是过期时长由 expireAfterSeconds 指定,单位为秒。3600 表示文档创建 1 小时后过期。
    ΔtTTL=expireAfterSeconds\Delta t_{\text{TTL}} = \text{expireAfterSeconds}
  3. t_expire 是过期时刻创建时间加上过期时长,得到该文档的精确过期时间点。
    texpire=tcreated+ΔtTTLt_{\text{expire}} = t_{\text{created}} + \Delta t_{\text{TTL}}
  4. 清理触发条件当系统当前时间大于 t_expire,后台线程就删除该文档。
    当前时间>texpire⇒删除\text{当前时间} > t_{\text{expire}} \Rightarrow \text{删除}

限制与实战:索引不是魔法

图书馆管理员如果给每个字段都做一张卡片,图书馆很快就会堆满卡片柜。更可怕的是,每次进新书,他都要同时更新几十个柜子,累得怀疑人生。聪明的管理员只会给“读者常查的字段”做卡片。

这就是 MongoDB 索引的限制与权衡。索引并非越多越好,因为每次插入、更新、删除文档,都要同步维护该集合上的每一个索引,这就是“写放大”。索引越多,写入越慢,磁盘占用越大。MongoDB 单个集合最多支持 64 个索引,索引键也有长度限制,超长键会导致索引构建失败。此外,查询优化器并不一定会用索引:当选择性太低或返回集占比过大时,优化器算一算成本,还是会选择全集合扫描。

选型建议可以总结为:优先为高频查询、排序、关联字段建索引;用 explain 验证索引是否生效;定期清理低效冗余索引;避免对每个字段都建索引。索引设计是“够用就好”的艺术,而不是“越多越好”的堆砌。

图书管理员最终的觉悟是:目录卡要精,不要多。MongoDB 索引设计同样如此,在查询性能和写入代价之间取得平衡,才是生产环境中的最佳实践。

Wtotal=Wdata+∑i=1mWindex(i)\displaystyle W_{\text{total}} = W_{\text{data}} + \sum_{i=1}^{m} W_{\text{index}}^{(i)}
总写入成本等于写数据本身的成本,加上所有索引的维护成本之和。索引越多,写放大越严重。
  1. W_data 是写数据本身的成本将文档写入集合的基本 I/O 开销。
    Wdata=数据写入成本W_{\text{data}} = \text{数据写入成本}
  2. m 是索引数量集合上创建的索引个数,也就是每次写入需要同步维护的 B-Tree 数量。
    m=索引个数m = \text{索引个数}
  3. W_index⁽ⁱ⁾ 是第 i 个索引的维护开销每个索引都要在 B-Tree 中插入、更新或删除对应键。
    Windex(i)=第 i 个索引的维护成本W_{\text{index}}^{(i)} = \text{第 i 个索引的维护成本}
  4. W_total 是总写放大索引越多,每次写入的消耗越大。这是“空间换时间”的隐形成本,必须谨慎权衡。
    Wtotal=Wdata+∑i=1mWindex(i)W_{\text{total}} = W_{\text{data}} + \sum_{i=1}^{m} W_{\text{index}}^{(i)}

AI 应用场景

电商商品搜索与筛选

阶段:数据存储与检索(AI 应用的后端数据服务)

解决的问题:在电商平台中,运营需要按“分类 + 价格”筛选和排序商品。传统做法是每次查询全集合扫描,商品量达到百万级时,接口响应动辄数秒。通过创建复合索引 {category: 1, price: 1},筛选和排序在一次索引扫描内完成,用户体验大幅提升。

具体案例:某电商 SKU 表有 800 万商品。运营后台经常按“3C 数码 + 价格升序”筛选商品。加复合索引前,查询耗时 3.2 秒;加索引后降到 12 毫秒,提升超过 260 倍。

物联网设备状态自动清理

阶段:数据生命周期管理(AI 训练与推理数据的预处理)

解决的问题:海量 IoT 设备持续上报心跳数据,历史数据占用大量存储,还会拖慢最近的查询。用 TTL 索引在时间字段上设置过期时间,数据库自动清理旧数据,无需额外编写定时任务。

具体案例:智能水表每 5 分钟上报一条状态,日增数据超百万条。对 reportedAt 字段建立 TTL 索引,设置 expireAfterSeconds: 604800(7 天),过期数据自动删除,存储成本下降 70%。

附近门店 LBS 查询

阶段:空间数据检索(基于位置的服务)

解决的问题:外卖 App 需要返回用户附近 3 公里的门店。传统做法是遍历全部门店计算经纬度距离,耗时高。用 2dsphere 地理索引加 $near 查询,数据库直接利用空间索引完成范围检索。

具体案例:某外卖平台门店表有 20 万条记录,对 location 字段建 2dsphere 索引后,附近门店查询从 500 毫秒降到 5 毫秒,支撑了高峰期每秒上千次的 LBS 请求。

Python 演示

python68 行
import randomimport time  class Collection:    """模拟 MongoDB 集合(无索引)"""     def __init__(self):        self.docs = []     def insert(self, doc):        self.docs.append(doc)     def find(self, field, value):        for doc in self.docs:            if doc.get(field) == value:                return doc        return None  class IndexedCollection:    """模拟带索引的 MongoDB 集合"""     def __init__(self):        self.docs = []        self.index = {}     def insert(self, doc):        self.docs.append(doc)     def build_index(self, field):        self.index = {}        for doc in self.docs:            key = doc.get(field)            self.index.setdefault(key, []).append(doc)     def find_with_index(self, field, value):        matched = self.index.get(value, [])        return matched[0] if matched else None  def main():    total = 100000    cities = ["北京", "上海", "广州", "深圳", "杭州"]    plain = Collection()    indexed = IndexedCollection()    for i in range(total):        city = random.choice(cities)        doc = {"_id": i, "city": city, "age": random.randint(18, 80)}        plain.insert(doc)        indexed.insert(doc)     indexed.build_index("city")     target = "深圳"    t0 = time.time()    r1 = plain.find("city", target)    t1 = time.time()    r2 = indexed.find_with_index("city", target)    t2 = time.time()     print("结果一致:", r1 == r2)    print("无索引耗时: %.3f ms" % ((t1 - t0) * 1000))    print("有索引耗时: %.3f ms" % ((t2 - t1) * 1000))    print("性能提升: %.1f 倍" % ((t1 - t0) / max(t2 - t1, 1e-6)))  main()
目的:在内存中模拟 10 万条文档,对比“无索引逐条扫描”和“有索引直接定位”的查找耗时,直观验证 MongoDB 索引的价值。
  1. self.index.setdefault(key, []).append(doc)为每个键值维护一个文档列表,相当于索引卡片把同一位置的多本书归到一起,这是建索引的核心步骤。
  2. indexed.build_index("city")模拟 MongoDB 的 createIndex({city: 1}),在查询前预先构建“城市 → 文档”的映射。
  3. plain.find("city", target)无索引查询,必须遍历全部文档逐一比对,对应 MongoDB 的集合扫描 COLLSCAN。
  4. indexed.find_with_index("city", target)有索引查询,直接通过键值拿到结果,对应索引扫描 IXSCAN。
  5. print("性能提升: %.1f 倍" % ...)输出两种查询方式的耗时对比,让你亲眼看到索引带来的量级提升。

常见误区

误区索引能加速查询,那我给每个字段都建上索引,所有查询不就都变快了吗?反正磁盘空间也不贵。

正解索引确实加速查询,但代价是写放大与存储膨胀。每一次插入、更新、删除都要同步维护每棵 B-Tree,索引越多,写入越慢、磁盘占用越大。正确的做法是只对高频查询和高选择性字段建索引,并用 explain 确认索引真正被使用。

为何常见:新手容易只看到“加索引后查询变快”的正面效果,忽视了写入路径上的维护成本,以为索引是免费的午餐。

误区建立了复合索引 `{a: 1, b: 1}`,那单独查 b 字段时,也一定能用这个索引加速吧?毕竟索引里包含 b。

正解复合索引遵循最左前缀原则,就像“先按姓氏再按名字”排列的通讯录。只提供名字,没法在通讯录里快速定位;只有查询条件包含最左侧字段 a 时,索引才可能被使用。设计复合索引时,应把等值查询字段放前面,范围排序字段放后面。

为何常见:把复合索引简单理解为多个单字段索引的叠加,不清楚索引键的排列顺序决定了它的匹配规则。

误区我明明对 city 字段建了索引,为什么 explain 显示的还是 COLLSCAN?是不是索引建失败了?

正解查询优化器会基于成本选择执行计划。如果字段选择性极低(比如只有两个取值的布尔字段),走索引回表可能比全集合扫描更慢,优化器就会放弃索引。索引不是“建了就一定生效”,要结合数据分布判断。

为何常见:忽略了优化器的成本模型,以为索引是强制生效的“魔法开关”,遇到 COLLSCAN 就怀疑索引没建好。

误区稀疏索引不就是允许重复值吗?正好和唯一索引相反?那它们俩是不是一个管重复、一个管缺失?

正解这是两个不同维度的概念。唯一索引约束的是“字段值不能重复”;稀疏索引决定的是“哪些文档进索引”,只索引存在该字段的文档。两者还可以组合:稀疏 + 唯一,表示“存在的值不重复,且缺失字段的文档不建索引”。

为何常见:把“是否允许重复”和“是否索引缺失字段”两个维度混为一谈,没有理解索引选项之间的正交关系。

误区我用 `find({name: /张/})` 查找所有名字里带“张”的人,肯定能走索引吧?正则在 MongoDB 里不是都被优化吗?

正解只有前缀正则(如 /^张/)能利用 B-Tree 的字典序走索引;包含匹配(如 /张/)需要在所有字符串中做子串匹配,无法利用有序键,只能全集合扫描。需要模糊搜索时,应优先考虑文本索引(text index)。

为何常见:以为任何形式的正则都能借助 B-Tree 的有序性加速,忽略了“有序”只能帮助前缀匹配的本质。

面试题

Q1. MongoDB 中默认使用的索引数据结构是?

单选题 · easy

  • A.哈希表结构
  • B.B-Tree 树结构← 正确答案
  • C.跳表结构
  • D.红黑树结构

答案:B。MongoDB 的索引默认基于 B-Tree 组织,WiredTiger 存储引擎同样采用 B-Tree 变体,它能高效支持等值、范围和排序查询。哈希表只适合等值查询;跳表和红黑树虽然有序,但节点扇出小,磁盘 I/O 次数更多,不适合大规模索引场景。

Q2. 在 Mongo shell 中创建单字段升序索引的命令是?

单选题 · easy

  • A.createIndex({a:1}) 命令← 正确答案
  • B.addIndex({a:1}) 命令
  • C.index({a:1}) 命令
  • D.buildIndex({a:1}) 命令

答案:A。MongoDB 创建索引的标准 API 是 db.collection.createIndex({a: 1}),其中 1 表示升序。其他三个命令都不是 Mongo 的合法接口。这道题提醒我们,索引操作要使用官方命令,避免凭直觉猜测 API。

Q3. 对于复合索引 {a: 1, b: 1},下列哪些查询可以使用该索引?

多选题 · mid

  • A.只查询字段 a 的等值查询← 正确答案
  • B.只查询字段 b 的等值查询
  • C.同时查 a 和 b 的等值查询← 正确答案
  • D.在 b 上做范围排序

答案:AC。复合索引遵循最左前缀原则。查询条件必须包含第一个字段 a,因此只查 a 可以走索引,同时查 a 和 b 也可以;但只查 b 或不含 a 的范围排序,都无法使用这个复合索引。这就像按“姓氏 + 名字”排列的通讯录,只提供名字是没法用的。

Q4. 关于覆盖索引(covered query),下列说法正确的是?

单选题 · mid

  • A.查询结果的全部字段都在索引中← 正确答案
  • B.索引覆盖集合中的所有文档
  • C.查询自动使用最优索引
  • D.索引自动覆盖多个集合

答案:A。覆盖索引指查询所需的全部字段都包含在索引条目内,MongoDB 无需回表读取原始文档,从而大幅减少 I/O。比如索引 {name:1, age:1},查询只返回这两个字段且条件仅涉及 name,就可以实现覆盖查询。B、C、D 都是对“覆盖”一词的误解。

Q5. MongoDB 中 TTL 索引的核心作用是?

单选题 · mid

  • A.加速时间字段的范围查询
  • B.定期删除过期的文档数据← 正确答案
  • C.记录文档的最后修改时间
  • D.限制索引的存活时长

答案:B。TTL 索引的本质是“自动过期删除”。在时间字段上设置 expireAfterSeconds 后,MongoDB 会定期删除超过该时长的文档,常用于日志、会话、验证码等场景。它并不用于加速时间范围查询,也不是记录修改时间或索引生命周期。

Q6. 下列哪些情况可能导致索引无法生效?

多选题 · hard

  • A.对索引字段套用函数表达式← 正确答案
  • B.索引字段选择性极低← 正确答案
  • C.使用非前缀正则进行匹配← 正确答案
  • D.使用前缀正则锚定匹配

答案:ABC。对索引字段套函数(如 $toLower)后,索引键无法直接匹配;选择性极低时,优化器会认为全集合扫描更划算;非前缀正则(如 /张/)无法利用 B-Tree 字典序。只有前缀正则 /^张/ 可以走索引,因此 D 是能生效的情况。

Q7. explain() 结果显示 stage 为 COLLSCAN 表示什么?

单选题 · easy

  • A.查询走了索引扫描
  • B.执行了全集合扫描← 正确答案
  • C.自动使用了覆盖索引
  • D.索引已被全部删除

答案:B。COLLSCAN 是 collection scan 的缩写,意为全集合扫描,即数据库逐条读取集合中的每个文档来匹配查询条件。与之相对的是 IXSCAN(索引扫描)。看到 COLLSCAN,通常意味着查询没有走索引,是慢查询的典型信号。

Q8. 关于单字段索引的排序方向,下列说法正确的是?

单选题 · hard

  • A.反向排序无法使用索引
  • B.单字段索引可双向扫描← 正确答案
  • C.必须另建反向索引才能排序
  • D.索引只能支持升序排序

答案:B。单字段 B-Tree 索引虽然按升序存储,但查询 sort({a: -1}) 时,MongoDB 可以从叶节点反向遍历同一个索引,无需额外创建降序索引。因此反向排序完全可以用现有索引,A、C、D 都误解了索引方向的作用方式。

Q9. 关于稀疏索引,下列说法正确的有哪些?

多选题 · mid

  • A.只索引存在该字段的文档← 正确答案
  • B.可以节省不少索引存储空间← 正确答案
  • C.本质上与唯一索引相同
  • D.能与唯一索引组合使用← 正确答案

答案:ABD。稀疏索引用 sparse: true 创建,只对“存在该字段”的文档建索引,因此能节省存储空间,适合字段存在性稀疏的场景。它与唯一索引是两个维度,可以组合成“稀疏 + 唯一”索引。C 把稀疏索引和唯一索引混为一谈,是错误的。

Q10. B-Tree 索引需要支持 100 万键,节点扇出约为 500,定位一个键需要读取多少层磁盘页(根节点常驻内存不计)?

单选题 · extreme

  • A.1 次
  • B.2 次
  • C.3 次← 正确答案
  • D.4 次

答案:C。树高公式为 h ≈ log₅₀₀(10⁶)。计算可得 ln(10⁶)/ln(500) ≈ 13.82 / 6.21 ≈ 2.22,向上取整为 3。根节点常驻内存不需要读磁盘,所以从第二层开始读取,大约需要读 3 层磁盘页。这个极端题考察了对 B-Tree 树高公式的真正理解。