Skip to content

MongoDB 索引原理

索引的作用

  1. 加速查询:减少查询时需要扫描的文档数量
  2. 支持排序:加速排序操作
  3. 强制唯一性:通过唯一索引确保字段值的唯一性
  4. 支持范围查询:提高范围查询的性能
  5. 优化聚合操作:加速聚合管道中的匹配和排序阶段

索引数据结构

B树索引

MongoDB使用B树作为默认的索引数据结构,B树具有以下特点:

  1. 平衡结构:B树是自平衡的,所有叶子节点位于同一层
  2. 多路搜索:每个节点可以有多个子节点,减少树的高度
  3. 有序存储:索引键按顺序存储,支持范围查询和排序
  4. 高效插入删除:插入和删除操作的时间复杂度为O(log n)
  5. 磁盘友好:节点大小设计为磁盘块大小,减少磁盘I/O

B树与B+树的区别

MongoDB使用的是B树而不是B+树,两者的主要区别:

特性B树B+树
数据存储所有节点都存储数据只有叶子节点存储数据
叶子节点连接没有有(双向链表)
范围查询需要回溯高效(通过叶子节点链表)
适合场景随机访问范围查询

索引结构示例

                 [ root ]
                /        \
         [ page 1 ]    [ page 2 ]
        /    |     \   /    |     \
[ leaf 1 ] [ leaf 2 ] [ leaf 3 ] [ leaf 4 ]

每个节点包含索引键和指向子节点或文档的指针:

  • 根节点:指向中间页或叶子页
  • 中间页:指向其他中间页或叶子页
  • 叶子页:存储索引键和指向文档的指针(或文档本身,对于覆盖索引)

索引类型

单字段索引

单字段索引是最基本的索引类型,基于单个字段创建索引:

javascript
// 创建单字段索引
db.collection.createIndex({ field: 1 })  // 升序索引
db.collection.createIndex({ field: -1 }) // 降序索引

复合索引

复合索引基于多个字段创建索引,字段顺序对索引性能有重要影响:

javascript
// 创建复合索引
db.collection.createIndex({ field1: 1, field2: -1 })

复合索引的前缀原则

复合索引支持前缀查询,例如索引 { a: 1, b: 1, c: 1 } 可以支持:

  • { a: ... }
  • { a: ..., b: ... }
  • { a: ..., b: ..., c: ... }

但不支持:

  • { b: ... }
  • { b: ..., c: ... }
  • { c: ... }

多键索引

当字段值是数组时,MongoDB会自动创建多键索引,为数组中的每个元素创建索引条目:

javascript
// 文档结构
{ _id: 1, tags: [ "mongodb", "database", "nosql" ] }

// 创建多键索引
db.collection.createIndex({ tags: 1 })

地理空间索引

用于地理位置数据的查询,支持点、线、面等地理空间数据类型:

javascript
// 2dsphere索引(用于地球表面的地理空间数据)
db.collection.createIndex({ location: "2dsphere" })

// 2d索引(用于平面地理空间数据)
db.collection.createIndex({ location: "2d" })

文本索引

用于全文搜索,支持对字符串内容进行分词和搜索:

javascript
// 创建文本索引
db.collection.createIndex({ content: "text" })

// 多字段文本索引
db.collection.createIndex({ title: "text", content: "text" })

哈希索引

基于字段的哈希值创建索引,用于相等查询,不支持范围查询:

javascript
// 创建哈希索引
db.collection.createIndex({ field: "hashed" })

唯一索引

确保索引字段的值是唯一的:

javascript
// 创建唯一索引
db.collection.createIndex({ field: 1 }, { unique: true })

// 复合唯一索引
db.collection.createIndex({ field1: 1, field2: 1 }, { unique: true })

稀疏索引

只包含具有索引字段的文档,跳过没有索引字段的文档:

javascript
// 创建稀疏索引
db.collection.createIndex({ field: 1 }, { sparse: true })

部分索引

基于过滤条件创建索引,只包含满足条件的文档:

javascript
// 创建部分索引
db.collection.createIndex(
  { field: 1 },
  { partialFilterExpression: { status: { $eq: "active" } } }
)

索引工作原理

查询优化器

MongoDB查询优化器会评估多个查询计划,选择成本最低的计划执行:

  1. 生成查询计划:针对每个可能的索引生成查询计划
  2. 评估计划成本:基于文档扫描数量、索引使用情况等评估成本
  3. 选择最佳计划:选择成本最低的计划执行
  4. 缓存计划:缓存查询计划,提高重复查询的性能

索引扫描类型

  1. IXSCAN:索引扫描,直接使用索引查找文档
  2. COLLSCAN:全表扫描,扫描整个集合查找文档
  3. FETCH:获取文档,从磁盘读取文档
  4. SORT:排序,对结果进行排序
  5. LIMIT:限制结果数量

索引使用示例

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

// 查询1:使用索引(前缀匹配)
db.users.find({ age: { $gt: 25 } })

// 查询2:使用索引(复合匹配)
db.users.find({ age: 30, name: { $regex: /^A/ } })

// 查询3:使用索引排序
db.users.find().sort({ age: 1, name: 1 })

// 查询4:不使用索引(不符合前缀原则)
db.users.find({ name: "Alice" })

索引创建与管理

创建索引

javascript
// 基本语法
db.collection.createIndex(
  { field1: 1, field2: -1 },  // 索引键和方向
  {                           // 索引选项
    name: "custom_index_name", // 索引名称
    unique: false,             // 是否唯一
    sparse: false,             // 是否稀疏
    expireAfterSeconds: 3600,  // TTL索引(秒)
    collation: { locale: "en" } // 排序规则
  }
)

查看索引

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

// 查看索引大小
db.collection.totalIndexSize()

删除索引

javascript
// 删除指定索引
db.collection.dropIndex("index_name")

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

重建索引

javascript
// 重建所有索引
db.collection.reIndex()

// 重建指定索引
db.collection.dropIndex("index_name")
db.collection.createIndex({ field: 1 }, { name: "index_name" })

索引性能优化

索引选择性

索引选择性是指索引字段的唯一值数量与集合中文档数量的比值,选择性越高,索引效果越好:

javascript
// 高选择性索引(唯一值多)
db.users.createIndex({ email: 1 })

// 低选择性索引(唯一值少)
db.users.createIndex({ gender: 1 })

覆盖索引

覆盖索引包含查询所需的所有字段,不需要额外的文档获取操作:

javascript
// 查询:db.users.find({ age: 30 }, { name: 1, email: 1, _id: 0 })

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

索引交集

MongoDB支持使用多个索引的交集来满足查询:

javascript
// 创建两个索引
db.collection.createIndex({ field1: 1 })
db.collection.createIndex({ field2: 1 })

// 查询使用索引交集
db.collection.find({ field1: "value1", field2: "value2" })

索引排序顺序

索引的排序顺序(升序/降序)会影响查询的性能,特别是对于复合索引:

javascript
// 查询排序顺序与索引一致
db.collection.find().sort({ field1: 1, field2: 1 })
// 对应的索引
db.collection.createIndex({ field1: 1, field2: 1 })

// 查询排序顺序与索引相反
db.collection.find().sort({ field1: -1, field2: -1 })
// 对应的索引(可以重用上面的索引)

索引监控与分析

解释计划

使用explain()方法分析查询计划:

javascript
// 查看查询计划
db.collection.find({ field: "value" }).explain()

// 详细模式
db.collection.find({ field: "value" }).explain("executionStats")

// 全量模式
db.collection.find({ field: "value" }).explain("allPlansExecution")

索引使用统计

查看索引的使用情况:

javascript
// 启用索引使用统计
db.setProfilingLevel(1, { slowms: 100 })

// 查看索引使用统计
db.system.profile.find({
  op: "query",
  "executionStats.executionStages.inputStage.stage": "IXSCAN"
}).limit(10)

索引扫描统计

javascript
// 查看索引扫描数量
db.serverStatus().indexCounters

// 查看集合级别的索引统计
db.collection.stats().indexDetails

索引最佳实践

  1. 为常用查询创建索引:分析查询模式,为频繁使用的查询创建索引
  2. 遵循前缀原则:合理设计复合索引的字段顺序
  3. 使用覆盖索引:减少磁盘I/O,提高查询性能
  4. 避免过度索引:每个索引会增加写操作的开销
  5. 定期监控索引使用:删除不使用或低效率的索引
  6. 使用部分索引:只对常用文档创建索引
  7. 考虑索引选择性:优先为选择性高的字段创建索引
  8. 使用TTL索引管理过期数据:自动删除过期文档
  9. 在低峰期创建索引:避免影响生产环境性能
  10. 测试索引性能:使用explain()分析索引的效果

索引限制

  1. 索引键大小限制:索引键的大小不能超过1024字节
  2. 索引数量限制:每个集合的索引数量建议不超过64个
  3. 复合索引字段限制:复合索引最多可以包含32个字段
  4. 内存限制:索引需要加载到内存中,占用内存资源
  5. 写操作开销:每次写操作需要更新所有相关索引
  6. 索引碎片:频繁的插入删除会导致索引碎片,影响性能

常见问题(FAQ)

Q1: 什么时候需要创建索引?

A1: 当查询性能不佳,或者查询计划显示全表扫描(COLLSCAN)时,应该考虑创建索引。特别是对于频繁执行的查询、范围查询、排序操作和聚合操作,索引可以显著提高性能。

Q2: 如何选择索引的字段顺序?

A2: 索引的字段顺序应该根据查询模式确定:

  • 首先放置选择性高的字段
  • 其次放置常用于精确匹配的字段
  • 最后放置用于范围查询或排序的字段

例如,对于查询 db.users.find({ age: 30, city: "Beijing" }).sort({ name: 1 }),最佳索引顺序是 { age: 1, city: 1, name: 1 }

Q3: 复合索引和多个单字段索引哪个更好?

A3: 这取决于查询模式:

  • 如果查询经常同时使用多个字段,复合索引更好
  • 如果查询经常只使用单个字段,多个单字段索引可能更好
  • MongoDB支持索引交集,可以组合使用多个单字段索引

Q4: 如何检测不使用的索引?

A4: 可以通过以下方法检测不使用的索引:

  • 使用数据库分析器(profiler)监控索引使用情况
  • 查看索引访问统计信息
  • 定期运行查询计划分析
  • 使用第三方工具(如MongoDB Compass、Ops Manager)分析索引使用情况

Q5: 索引会影响写操作性能吗?

A5: 是的,索引会增加写操作的开销。每次插入、更新或删除文档时,MongoDB需要更新所有相关的索引。因此,应该避免过度索引,只创建必要的索引。

Q6: 如何优化索引碎片?

A6: 优化索引碎片的方法:

  • 定期重建索引(db.collection.reIndex())
  • 使用compact命令压缩集合
  • 对于频繁更新的集合,考虑使用更大的索引页大小
  • 避免频繁的插入删除操作

Q7: 唯一索引和稀疏索引可以一起使用吗?

A7: 是的,唯一索引可以和稀疏索引一起使用:

javascript
db.collection.createIndex({ field: 1 }, { unique: true, sparse: true })

这会创建一个唯一索引,只包含具有该字段的文档,并且确保这些文档的字段值是唯一的。

Q8: TTL索引是如何工作的?

A8: TTL(Time To Live)索引自动删除过期文档:

  • 基于日期类型字段创建
  • 设置expireAfterSeconds参数指定过期时间
  • MongoDB后台线程定期检查并删除过期文档
  • 只能用于单字段索引
  • 不支持复合索引

Q9: 如何为数组字段创建索引?

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

javascript
db.collection.createIndex({ arrayField: 1 })

对于嵌套数组,可以使用点表示法创建索引:

javascript
db.collection.createIndex({ "arrayField.nestedField": 1 })

Q10: 索引键大小超过限制怎么办?

A10: 如果索引键大小超过1024字节,可以考虑以下解决方案:

  • 使用哈希索引(只适用于相等查询)
  • 缩短字段值长度
  • 使用部分索引,只索引常用值
  • 考虑使用其他查询策略,如全文搜索或正则表达式索引