MongoDB 索引与查询优化

MongoDB 系统讲解第一篇:02 篇已有索引入门(createIndex/explain 初体验),这一篇把索引讲全讲透——类型全家桶、复合索引的 ESR 原则、explain 输出逐字段怎么读。

索引类型全家桶

db.players.createIndex({"level": 1})                        // 单字段,1 升序 -1 降序
db.players.createIndex({"server": 1, "level": -1})          // 复合索引
db.players.createIndex({"createdAt": 1}, {expireAfterSeconds: 2592000})  // TTL 索引:30 天后自动删
db.players.createIndex({"name": 1}, {unique: true})         // 唯一索引
db.players.createIndex({"email": 1}, {partialFilterExpression: {vip: true}})  // 部分索引:只索引 vip 用户
db.players.createIndex({"desc": "text"})                    // 文本索引(全文搜索)
db.players.createIndex({"location": "2dsphere"})            // 地理索引
  • TTL 索引是"过期自动清理"的官方方案——会话、日志类集合标配,省掉自己写定时清理任务
  • 部分索引只为符合条件的子集建索引:索引更小更快,"只查活跃用户"这类场景专用
  • 唯一索引注意:字段缺失时 null 也算重复值,多个文档缺字段会撞唯一约束(sparse 或部分索引可绕)

复合索引与 ESR 原则

复合索引 {"a": 1, "b": 1, "c": 1} 相当于电话簿按 a→b→c 排序。字段顺序决定索引能不能被用上、能用多少

ESR(Equality - Sort - Range)

  1. E:等值条件的字段放最前
  2. S:排序字段放其次
  3. R:范围查询的字段放最后

例:查询 {"server": "X"}level 排序、再筛 {"level": {"$gt": 20}}——索引应该是 {"server": 1, "level": 1}(server 是 E,level 先承担排序再承担范围,两者重合)。

💡 为什么 R 要放最后:范围字段之后的索引字段就"断档"了——按 a 查、b 范围查、c 精确查的索引 {"a":1,"b":1,"c":1},b 的范围性让 c 无法走索引前缀,只能过滤。ESR 记忆钩子:先等值、再排序、最后范围

前缀规则{a,b,c} 的索引可支撑 {a}{a,b} 的查询,但撑不起 {b}——所以"每个查询建一个索引"是错的,按查询族设计复合索引。

explain:三种模式与关键字段

db.players.find({"server": "东方明珠", "level": {"$gt": 20}})
          .sort({"level": -1})
          .explain("executionStats")     // 最常用:真的执行并给统计

逐字段读法(executionStats 层):

字段 含义 期望
nReturned 返回条数
totalKeysExamined 走索引扫了多少条 与 nReturned 接近
totalDocsExamined 回表读了多少文档 与 nReturned 接近=索引高效
executionTimeMillis 耗时

三个红旗:COLLSCAN(全集合扫描,该建索引了);totalDocsExamined >> nReturned(索引选得不准或 ESR 顺序不对);SORT stage 内存排序(排序没被索引吃掉,数据量大时可能报内存超限错误)。

索引的代价与设计清单

索引不是免费的:

  • 写放大:每次 insert/update 都要同步维护所有相关索引
  • 内存占用:索引要常驻内存(WiredTiger 缓存)才快,被挤出去就是灾难
  • 数量红线:单集合索引别无脑堆,书里的常见建议是生产集合控制在个位数到十来个,冗余索引(被复合索引前缀覆盖的单字段索引)定期清理

我的设计清单(书里经验的汇总):

  1. 先看 explain 确认真的是索引问题,再建索引
  2. 复合索引按 ESR 排,覆盖常用查询族
  3. 排序字段尽量进索引,避免内存 SORT
  4. 分页别用大 skip(翻得越深越慢),用 {"_id": {"$gt": 上一页最后一个}} 基于游标翻页
  5. TTL/部分索引按需上,用它们省内存

⬅️ 03-MongoDB 理解 🏠 00-数据库 ➡️ 05-MongoDB 副本集与分片