跳到主要内容

索引

索引类型​

类型创建用途
单字段{age: 1}基础。1 升序 -1 降序
复合索引{a: 1, b: -1}多条件查询,最多 32 个字段
多键索引 Multikey对数组字段建索引时自动成为多键为数组每个元素建一个索引项
文本索引 Text{title: "text"}全文搜索,每个集合只能有一个
哈希索引 Hashed{_id: "hashed"}只支持等值查询,用于哈希分片
地理空间2dsphere / 2d附近的人、范围查询
通配符 Wildcard{"$**": 1}字段名不确定时(4.2+)
TTL 索引{createdAt: 1}, {expireAfterSeconds: 3600}自动删除过期文档
部分索引 PartialpartialFilterExpression只对满足条件的文档建索引,省空间
稀疏索引 Sparse{sparse: true}跳过不含该字段的文档
唯一索引{unique: true}唯一约束

TTL 索引的三个考点:

  1. 后台任务每 60 秒扫描一次,所以删除有延迟,不保证精确到秒
  2. 字段必须是 Date 类型或 Date 数组,其他类型不会被删
  3. 要做「指定时间点过期」,设 expireAfterSeconds: 0 并把字段值设为目标过期时间

部分索引 vs 稀疏索引:部分索引更通用(可以写任意条件),官方推荐优先用它。典型用法是只给「未删除的订单」建索引:partialFilterExpression: {isDeleted: false}。

🔥 ESR 规则​

复合索引的字段顺序法则,MongoDB 面试的标志性考点:

Equality → Sort → Range
等值匹配 排序 范围查询

为什么是这个顺序:

  • 等值字段放最前 —— 把 B+ 树的扫描范围缩小到一个连续区间
  • 排序字段放中间 —— 在等值确定的前缀内,索引本身已经有序,可以直接利用索引顺序返回结果,避免内存排序
  • 范围字段放最后 —— 范围查询之后的索引字段无法再用于排序或精确匹配(和 MySQL 的最左前缀一样)

例子:查询 {status: "active", age: {$gt: 18}} 并 sort({createdAt: -1})

// ❌ 错:range 在 sort 之前,createdAt 用不上索引排序 → SORT 阶段
db.t.createIndex({status: 1, age: 1, createdAt: -1})

// ✅ 对:E → S → R
db.t.createIndex({status: 1, createdAt: -1, age: 1})

:::warning 排序方向必须匹配 索引 {a: 1, b: -1} 能支持 sort({a: 1, b: -1}),也能支持完全反向的 sort({a: -1, b: 1})(反向扫描索引)。但不能支持 sort({a: 1, b: 1}) —— 方向组合不一致。

这个坑在「列表页支持多种排序」的需求里经常踩到。 :::

最左前缀与覆盖查询​

最左前缀和 MySQL 完全一样:索引 {a:1, b:1, c:1} 能服务 {a}、{a,b}、{a,b,c} 的查询,不能服务只有 {b} 或 {c} 的查询。

覆盖查询(covered query) —— 查询和投影的字段全在索引里,不需要读取文档。条件比 MySQL 严格:

  1. 查询条件的所有字段 + 返回的所有字段都在同一个索引里
  2. 必须显式排除 _id({_id: 0}),因为 _id 默认返回但通常不在该索引里
  3. 索引字段不能是数组(多键索引无法覆盖)
db.t.createIndex({name: 1, age: 1})
db.t.find({name: "张三"}, {name: 1, age: 1, _id: 0}) // ✅ 覆盖
db.t.find({name: "张三"}, {name: 1, age: 1}) // ❌ _id 导致要读文档

验证方法:explain() 的 winningPlan.stage 是 PROJECTION_COVERED,且 executionStats.totalDocsExamined 为 0。

多键索引的限制​

对数组字段建索引会自动变成多键索引(不需要声明)。三个限制:

  1. 一个复合索引里最多有一个字段是数组 —— {a: 1, b: 1} 中 a 和 b 不能同时是数组(否则索引项是笛卡尔积,会爆炸)
  2. 不能作为分片键
  3. 不能用于覆盖查询

另外多键索引的索引项数量 = 所有文档的数组元素总数,所以给大数组建索引非常占空间。

explain 怎么读​

db.t.find({...}).explain("executionStats")

三种模式:queryPlanner(只看计划,默认)、executionStats(执行并给统计)、allPlansExecution(含被淘汰的候选计划)。

关键字段:

字段含义
winningPlan.stage执行阶段类型,见下表
nReturned实际返回的文档数
totalKeysExamined扫描的索引键数
totalDocsExamined扫描的文档数
executionTimeMillis耗时

stage 的含义(对应 MySQL 的 type):

stage含义好坏
IDHACK按 _id 查询的快速路径✅ 最好
PROJECTION_COVERED覆盖查询,不读文档✅ 很好
IXSCAN索引扫描✅ 好
FETCH用索引结果去取文档(≈ 回表)正常
SORT内存排序,没用上索引排序⚠️ 要优化
COLLSCAN全集合扫描❌ 最差

判断健康的核心指标:totalDocsExamined / nReturned 应该接近 1。等于 1 说明扫一个返回一个,索引精准;远大于 1 说明扫了大量无用文档。

totalKeysExamined 远大于 nReturned 则说明索引字段顺序不对(扫了很多索引项但大部分被过滤掉)。

:::tip SORT 阶段的硬限制 内存排序有 100MB 上限(internalQueryMaxBlockingSortMemoryUsageBytes),超了直接报错 Sort exceeded memory limit。只能靠建对索引消除 SORT 阶段,或者在聚合里开 allowDiskUse。

生产上「列表接口偶发 500」的一个典型原因就是这个 —— 数据量涨上来之后排序超限。 :::

其他要点​

索引的代价:每个索引都要在写入时维护,且占用 WiredTiger cache。索引总大小应该能装进内存,否则索引本身的随机 IO 会成为瓶颈。查看:db.t.totalIndexSize()。

后台建索引 —— 4.2 之前要写 {background: true} 否则会锁住整个数据库;4.2+ 的新建索引算法默认就不阻塞读写,不再需要这个选项。

找出无用索引 —— $indexStats 聚合可以看每个索引被使用的次数:

db.t.aggregate([{$indexStats: {}}])

从未被使用(accesses.ops 为 0)且运行足够久的索引可以删掉。

和 MySQL 索引的区别(高频对比题):

MongoDBMySQL (InnoDB)
数据结构B+ 树B+ 树
聚簇默认非聚簇,连 _id 都要「回表」主键聚簇
数组字段支持(多键索引)不支持(只能用生成列或全文索引变通)
嵌套字段支持 "a.b.c" 直接建索引需要 JSON 虚拟列 + 索引
TTL 自动过期内置无,要自己写定时任务
单集合/表索引上限64无硬限制(建议 5~6)