跳到主要内容

索引优化

索引是提升查询性能的关键。MongoDB 支持多种类型的索引,本章将详细介绍索引的类型、创建和使用。

索引基础

什么是索引?

索引是一种数据结构,能够快速定位数据,类似于书籍的目录:

无索引:全表扫描
[文档1] → [文档2] → [文档3] → ... → [文档N]
↓ 逐个比对
找到目标

有索引:快速查找
索引 → [指向文档的指针] → 目标文档

索引类型

索引类型说明使用场景
单字段索引索引单个字段常见查询条件
复合索引索引多个字段多条件查询
多键索引索引数组字段数组查询
文本索引全文搜索文本搜索
地理空间索引地理位置查询附近地点搜索
哈希索引哈希值索引分片键
唯一索引唯一值约束防止重复

创建索引

createIndex 方法

// 基本语法
db.collection.createIndex(<keys>, <options>)

// 简化写法(升序)
db.users.createIndex({ age: 1 })

// 详细写法
db.users.createIndex({ age: 1 }, { name: "age_index" })

单字段索引

// 为 age 字段创建索引
db.users.createIndex({ age: 1 }) // 1 升序,-1 降序

// 为嵌套字段创建索引(点符号)
db.orders.createIndex({ "customer.city": 1 })

// 为数组字段创建索引(自动成为多键索引)
db.products.createIndex({ tags: 1 })

复合索引

复合索引包含多个字段,字段顺序很重要:

// 创建复合索引
db.orders.createIndex({ status: 1, createdAt: -1 })

// 复合索引支持的查询模式
// 1. 使用索引前缀字段
db.orders.find({ status: "completed" })

// 2. 使用所有字段(顺序无关,可使用索引)
db.orders.find({ status: "completed", createdAt: { $gt: new Date() } })

// 3. 只使用部分字段(跳过前缀,索引效率低)
db.orders.find({ createdAt: { $gt: new Date() } })

复合索引字段顺序选择原则

  1. 字段基数(唯一值数量)高的放前面
  2. 等值查询频繁的字段放前面
  3. 范围查询放后面

多键索引

为数组字段创建索引时会自动创建多键索引:

// tags 是数组字段
db.products.createIndex({ tags: 1 })

// 数组中任意元素匹配都可以使用索引
db.products.find({ tags: "electronics" })

唯一索引

// 创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true })

// 复合唯一索引
db.orders.createIndex({ customerId: 1, orderId: 1 }, { unique: true })

文本索引

用于全文搜索:

// 创建文本索引(可指定权重和语言)
db.articles.createIndex(
{ title: "text", content: "text" },
{ weights: { title: 10, content: 1 }, default_language: "chinese" }
)

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

// 文本搜索评分
db.articles.find(
{ $text: { $search: "MongoDB" } },
{ score: { $meta: "textScore" } }
)

地理空间索引

// 2dsphere 索引(地球表面)
db.places.createIndex({ location: "2dsphere" })

// 2d 索引(平面)
db.places.createIndex({ location: "2d" })

哈希索引

主要用于分片:

db.users.createIndex({ _id: "hashed" })

索引管理

查看索引

// 查看集合的所有索引
db.users.getIndexes()

// 查看索引详情
db.users.getIndexSpecs()

删除索引

// 按名称删除
db.users.dropIndex("age_1")

// 按索引键删除
db.users.dropIndex({ age: 1 })

// 删除所有索引(保留 _id 索引)
db.users.dropIndexes()

查看索引使用情况

// 查看查询是否使用了索引
db.users.find({ age: 25 }).explain()

// explain 输出的关键字段
// winningPlan.inputStage.indexName - 使用的索引
// winningPlan.inputStage.indexBounds - 索引范围

索引属性

// 稀疏索引(只索引非空值)
db.users.createIndex({ email: 1 }, { sparse: true })

// 生存时间索引(TTL,自动过期删除)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // 1小时后自动删除
)

// 复合索引的顺序
db.orders.createIndex(
{ status: 1, createdAt: -1 },
{ background: true } // 后台创建
)

索引优化策略

1. 选择合适的索引字段

// 高基数字段(唯一值多)优先
// 好的索引
db.users.createIndex({ email: 1 }) // email 唯一性高

// 不好的索引
db.users.createIndex({ gender: 1 }) // gender 只有几个值

2. 创建复合索引

// 场景:经常查询 "状态=已完成" 且 "创建时间"
db.orders.createIndex({ status: 1, createdAt: -1 })

// 不需要为每个查询创建单独的索引
// 复合索引 { status: 1, createdAt: -1 } 可以支持:
// find({ status: "completed" })
// find({ status: "completed", createdAt: { $gt: ... } })
// find({ status: { $in: [...] }, createdAt: { $gt: ... } })

3. 覆盖查询

索引覆盖是指查询只需要返回索引中的字段,不需要回表查询:

// 创建索引
db.users.createIndex({ age: 1, name: 1 })

// 覆盖查询:只返回索引中的字段
db.users.find(
{ age: 25 },
{ _id: 0, name: 1 } // 只返回 name,不返回 _id
)

// 不覆盖:需要返回未索引的字段
db.users.find({ age: 25 }, { name: 1, email: 1 }) // email 未索引,需要回表

4. 避免索引过多

索引会占用空间并影响写入性能:

// 监控索引大小
db.users.stats().indexSizes

// 评估查询是否使用索引
db.users.find({ age: { $gt: 20 } }).explain("executionStats")

实战示例

1. 电商订单查询优化

// 场景:经常按订单状态和日期查询
db.orders.createIndex({ status: 1, createdAt: -1 })

// 场景:按用户查询订单
db.orders.createIndex({ customerId: 1, createdAt: -1 })

// 场景:按订单号精确查询
db.orders.createIndex({ orderId: 1 }, { unique: true })

2. 用户登录优化

// 场景:按邮箱登录
db.users.createIndex({ email: 1 }, { unique: true })

// 场景:按用户名查询
db.users.createIndex({ username: 1 })

// 复合索引(如果有按用户名+状态的查询)
db.users.createIndex({ username: 1, status: 1 })

3. 日志数据自动清理

// 创建 TTL 索引,7 天后自动删除
db.logs.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 7 * 24 * 60 * 60 }
)

4. 全文搜索优化

// 为文章创建文本索引
db.articles.createIndex(
{ title: "text", content: "text", tags: "text" },
{
weights: { title: 10, content: 5, tags: 3 },
default_language: "chinese"
}
)

// 搜索优化:限定返回字段
db.articles.find(
{ $text: { $search: "MongoDB" } },
{ title: 1, _id: 0, score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

索引最佳实践

  1. 使用 explain() 分析查询:了解查询是否使用了索引
  2. 监控慢查询:使用数据库分析器找出需要优化的查询
  3. 定期维护索引:删除不再使用的索引
  4. 考虑查询覆盖:减少回表查询
  5. 平衡读和写:索引能提升读性能,但会影响写性能
// 分析查询计划
db.orders.find({ status: "pending" }).explain("allPlansExecution")

// 查看慢查询阈值(系统默认 100ms)
db.getProfilingLevel()
db.getProfilingStatus()

// 设置慢查询日志
db.setProfilingLevel(1, 100) // 记录超过 100ms 的查询

小结

本章我们学习了:

  1. 索引基础:索引概念和原理
  2. 索引类型:单字段、复合、多键、文本、地理空间、哈希索引
  3. 索引创建:createIndex 方法和选项
  4. 索引管理:查看、删除、优化
  5. 索引优化策略:字段选择、复合索引、覆盖查询
  6. 实战示例:电商、用户、日志、搜索场景的索引设计