Skip to content

MongoDB 索引

为什么需要索引

无索引时 MongoDB 必须执行全集合扫描(COLLSCAN),逐一检查每个文档。索引通过 B-tree 数据结构存储字段的排序子集,大幅加速查询。

⚠️ 索引加速读取,但会增加写入开销。每次写入都需同步更新索引。

索引类型

类型用途示例
单字段单字段查询{ age: 1 }
复合多字段查询{ userId: 1, createdAt: -1 }
多键数组字段{ tags: 1 }(自动识别)
文本全文搜索{ description: "text" }
地理空间位置查询{ location: "2dsphere" }
哈希分片 / 等值查询{ userId: "hashed" }
TTL自动过期{ expireAt: 1 } + expireAfterSeconds
通配符动态 Schema{ "$**": 1 }

创建索引

单字段索引

js
// 1 = 升序
db.users.createIndex({ age: 1 })

// -1 = 降序(对单字段索引无实质影响)
db.users.createIndex({ createdAt: -1 })

复合索引

js
db.orders.createIndex({ userId: 1, createdAt: -1 })

复合索引遵循最左前缀匹配{ userId: 1, createdAt: -1 } 可覆盖 { userId } 查询,但无法覆盖 { createdAt } 查询。

ESR 规则 — 复合索引字段顺序

Equality → Sort → Range
  =         排序     范围
js
// 查询:category = "electronics" AND price > 50,按 name 排序
db.products.find({ category: "electronics", price: { $gt: 50 } })
           .sort({ name: 1 })

// 最优索引
db.products.createIndex({ category: 1, name: 1, price: 1 })
//                        =           排序       范围

唯一索引

js
db.users.createIndex({ email: 1 }, { unique: true })

TTL 索引

js
// 文档在 expireAt 时间后自动删除
db.sessions.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })

// 30 分钟后自动删除
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 1800 })

文本索引

js
// 创建(一个集合最多一个文本索引)
db.articles.createIndex({ title: "text", body: "text" })

// 搜索
db.articles.find({ $text: { $search: "MongoDB performance" } })

地理空间索引

js
db.places.createIndex({ location: "2dsphere" })

// 附近搜索
db.places.find({
  location: {
    $nearSphere: {
      $geometry: { type: "Point", coordinates: [-73.9667, 40.78] },
      $maxDistance: 5000  // 米
    }
  }
})

坐标格式为 [longitude, latitude](经度在前)。

部分索引(Partial Index)

js
// 仅为满足条件的文档建索引,节省空间
db.orders.createIndex(
  { userId: 1 },
  { partialFilterExpression: { status: "active" } }
)

分析索引性能

explain()

js
db.users.find({ age: { $gte: 25 } }).explain("executionStats")

关注指标:

指标含义
IXSCAN已使用索引 ✅
COLLSCAN全表扫描,缺少索引 ❌
totalDocsExamined扫描文档数,应接近 nReturned
totalDocsExamined: 0覆盖查询(最佳)

覆盖查询(Covered Query)

当索引包含查询和投影所需的全部字段时,MongoDB 只读索引不读文档:

js
// 索引覆盖所有字段
db.users.createIndex({ name: 1, age: 1 })

db.users.find(
  { name: "Alice" },
  { name: 1, age: 1, _id: 0 }
).explain("executionStats")
// totalDocsExamined: 0  ← 覆盖查询!

查看索引使用情况

js
// 索引统计
db.users.aggregate([{ $indexStats: {} }])

// 列出所有索引
db.users.getIndexes()

// 删除索引
db.users.dropIndex("age_1")

参考