索引设计与调优

索引类型

类型用途
单字段索引单列等值/范围/排序
复合索引多列组合,遵循前缀规则
多键索引(Multikey)数组字段自动成为多键索引
文本索引全文检索(text
地理索引2dsphere / 2d 地理查询
唯一索引保证唯一
TTL 索引到期自动删除文档(日志、验证码)
部分索引只索引满足条件的文档
稀疏索引跳过缺失字段
通配符索引索引未知/动态字段
db.users.createIndex({ email: 1 }, { unique: true })
db.orders.createIndex({ userId: 1, createdAt: -1 })       // 复合索引
db.orders.createIndex({ userId: 1 }, { partialFilterExpression: { status: "paid" } })
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })  // TTL
db.products.createIndex({ name: "text", description: "text" })

复合索引与 ESR 规则

复合索引同样遵循「最左前缀」。字段顺序按 ESR 排列:

  • E(Equality) 等值字段放最前
  • S(Sort) 排序字段居中
  • R(Range) 范围字段放最后
// 查询:db.orders.find({ userId: 7, status: "paid" })
//        .sort({ createdAt: -1 })
// E: userId, S: createdAt, R: 无
db.orders.createIndex({ userId: 1, createdAt: -1 })
Tip

ESR 与关系型联合索引「等值在前、范围在后」的思想一致,额外强调排序字段的位置。排序方向也要与索引一致:{ createdAt: -1 } 对应索引 -1,反向排序(全字段取反)也能用索引,但混合方向(一个升一个降)在 MongoDB 中需要注意索引方向定义。

覆盖查询

查询字段与返回字段都在索引中时,无需读取文档(totalDocsExamined: 0),效率最高:

db.users.createIndex({ email: 1, name: 1 })

// 只返回索引内字段,排除 _id
db.users.find(
  { email: "a@example.com" },
  { _id: 0, email: 1, name: 1 }
)

用 explain() 分析

db.orders.find({ userId: 7, status: "paid" }).sort({ createdAt: -1 }).explain("executionStats")

关注指标:

指标含义
winningPlan.stageCOLLSCAN(全表扫描,差)/ IXSCAN(索引,好)
totalKeysExamined扫描的索引键数
totalDocsExamined扫描的文档数
executionTimeMillis执行耗时
nReturned返回文档数

理想情况:IXSCAN + nReturned ≈ totalKeysExamined ≈ totalDocsExamined(覆盖查询时 DocsExamined = 0)。

Warning

totalDocsExamined 远大于 nReturned,说明索引选择性差或未覆盖;若出现 SORT / SORT_KEY_GENERATOR 阶段,说明排序未用索引,会占用大量内存。COLLSCAN 出现在大集合上通常是严重问题。

索引代价与维护

  • 每个索引都占用内存与磁盘,并拖慢写入。
  • 删除无用索引:db.coll.dropIndex("name")
  • 查看索引使用情况:
db.orders.aggregate([{ $indexStats: {} }])   // 每个索引的访问次数
db.orders.getIndexes()
// 强制使用索引(谨慎)
db.orders.find({ userId: 7 }).hint({ userId: 1, createdAt: -1 })
// 在副本集上建索引默认不影响可用性;在老版本可用 background
db.orders.createIndex({ userId: 1 }, { background: true })
Tip

定期用 $indexStats 找出 accesses.ops = 0 的索引并清理。索引不是越多越好——尤其在写多读少的集合上,多余索引会显著拖慢写入。

小结

  • 复合索引按 ESR(等值 → 排序 → 范围)设计字段顺序。
  • 覆盖查询(totalDocsExamined = 0)性能最佳。
  • explain("executionStats") 判断是否 IXSCAN、扫描是否高效。
  • 定期审计并清理无用索引,控制写入成本。