Skip to content

MongoDB 索引最佳实践

索引设计原则

1. 理解查询模式

在设计索引之前,必须深入了解应用程序的查询模式:

  • 分析最频繁执行的查询
  • 查看查询条件(filter)和排序(sort)字段
  • 检查聚合查询的分组和匹配条件
  • 识别复合查询的字段组合

2. 遵循 ESR 原则

对于复合索引,遵循 ESR(Equality, Sort, Range)原则:

  • Equality:首先添加等值匹配字段(如 field: value
  • Sort:然后添加排序字段
  • Range:最后添加范围查询字段(如 field > value

示例

javascript
// 查询模式
db.collection.find({ status: "active", category: "A" }).sort({ created_at: -1 }).limit(10)

// 最优复合索引
db.collection.createIndex({ status: 1, category: 1, created_at: -1 })

3. 考虑选择性

选择选择性高的字段作为索引前缀:

  • 选择性 = 不同值的数量 / 文档总数
  • 高选择性字段(如 user_id、email)适合作为索引前缀
  • 低选择性字段(如 status、type)不适合作为索引前缀

4. 避免过度索引

  • 每个索引都会增加写入操作的开销
  • 权衡查询性能提升与写入性能影响
  • 删除不使用或很少使用的索引

索引创建最佳实践

1. 在线创建索引

在生产环境中,使用 background: true 选项在线创建索引:

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

2. 使用复合索引替代多个单字段索引

对于经常一起查询的字段,创建复合索引比多个单字段索引更高效:

javascript
// 不推荐:多个单字段索引
db.collection.createIndex({ field1: 1 })
db.collection.createIndex({ field2: 1 })

// 推荐:复合索引
db.collection.createIndex({ field1: 1, field2: 1 })

3. 考虑索引大小

  • 索引越大,内存占用越高,性能越差
  • 避免在大型数组或文本字段上创建索引
  • 考虑使用部分索引只索引特定文档

4. 使用部分索引

对于特定查询模式,使用部分索引只索引满足条件的文档:

javascript
// 只索引 status 为 active 的文档
db.collection.createIndex(
  { field: 1 },
  { partialFilterExpression: { status: "active" } }
)

5. 使用稀疏索引

对于包含 null 值较多的字段,使用稀疏索引:

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

索引维护最佳实践

1. 定期监控索引使用情况

使用 db.collection.aggregate([{ $indexStats: {} }]) 监控索引使用情况:

javascript
// 查看索引使用统计
db.collection.aggregate([
  { $indexStats: {} },
  { $sort: { accesses: -1 } }
])

2. 重建碎片化索引

当索引碎片化严重时,重建索引以提高性能:

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

// 或删除并重新创建
db.collection.dropIndex({ field: 1 })
db.collection.createIndex({ field: 1 }, { background: true })

3. 监控索引大小

定期检查索引大小,及时优化过大的索引:

javascript
// 查看集合和索引统计信息
db.collection.stats()

4. 定期审查索引

  • 至少每季度审查一次索引
  • 删除未使用的索引
  • 优化低效索引
  • 更新不适合当前查询模式的索引

查询优化最佳实践

1. 使用 explain() 分析查询

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

javascript
// 分析查询执行计划
db.collection.find({ field: "value" }).sort({ created_at: -1 }).explain("executionStats")

2. 确保查询使用索引

  • 避免全表扫描(COLLSCAN)
  • 确保查询条件与索引前缀匹配
  • 避免在索引字段上使用函数

3. 优化排序操作

  • 确保排序字段在索引中
  • 避免混合排序方向(如 { field1: 1, field2: -1 })
  • 限制排序结果集大小

4. 优化范围查询

  • 将范围查询字段放在索引末尾
  • 避免在范围查询字段上使用排序
  • 限制范围查询结果集大小

特定场景最佳实践

1. 文本搜索

  • 使用文本索引进行全文搜索
  • 限制文本索引字段数量
  • 优化文本搜索查询
javascript
// 创建文本索引
db.collection.createIndex({ title: "text", content: "text" })

// 优化的文本搜索
db.collection.find({ $text: { $search: "keyword" } }).limit(10)

2. 地理空间查询

  • 根据查询类型选择合适的地理空间索引:
    • 2d 索引:平面地理数据
    • 2dsphere 索引:球形地理数据
  • 限制地理空间查询结果集大小

3. 时间序列数据

  • 使用复合索引:
  • 考虑使用 TTL 索引自动过期旧数据
  • 按时间范围分区数据
javascript
// TTL 索引:7天后自动删除
db.collection.createIndex({ created_at: 1 }, { expireAfterSeconds: 604800 })

4. 分片集群索引

  • 在所有分片上创建相同的索引
  • 考虑分片键与查询字段的关系
  • 避免在分片键上创建过多索引

索引性能监控

1. 关键指标

监控以下索引相关指标:

  • 索引命中率(Index Hit Ratio)
  • 全表扫描次数(COLLSCAN)
  • 索引扫描次数(IXSCAN)
  • 慢查询次数
  • 索引大小增长趋势

2. 监控工具

  • MongoDB Atlas 内置监控
  • Prometheus + Grafana
  • Datadog
  • New Relic

常见索引问题及解决方案

1. 索引未被使用

问题:创建了索引,但查询未使用该索引

解决方案

  • 检查查询条件是否与索引前缀匹配
  • 确保索引类型与查询类型匹配
  • 分析查询执行计划,查看是否存在更优索引

2. 索引过大

问题:索引大小超过预期,占用过多内存

解决方案

  • 考虑使用部分索引
  • 优化复合索引字段顺序
  • 删除不必要的索引字段
  • 重建碎片化索引

3. 写入性能下降

问题:索引过多导致写入性能下降

解决方案

  • 删除不使用的索引
  • 合并多个单字段索引为复合索引
  • 考虑使用稀疏索引或部分索引

版本差异

MongoDB 4.2+ 特性

  • 支持在线创建和删除索引
  • 支持索引构建过程中的取消操作
  • 改进了复合索引的内存使用

MongoDB 5.0+ 特性

  • 支持隐藏索引(hidden: true),用于测试新索引
  • 改进了索引统计信息收集
  • 支持更灵活的部分索引

MongoDB 6.0+ 特性

  • 支持向量索引(用于AI/ML应用)
  • 改进了索引构建的并行性
  • 支持索引压缩

常见问题(FAQ)

Q1: 如何确定哪些索引需要删除?

A1: 使用以下方法确定需要删除的索引:

  • 使用 $indexStats 聚合查看索引使用情况
  • 分析慢查询日志,识别未使用的索引
  • 检查索引大小和写入性能影响
  • 删除创建后从未使用过的索引

Q2: 复合索引的字段顺序如何影响查询性能?

A2: 复合索引的字段顺序对查询性能有显著影响:

  • 索引前缀匹配原则:查询必须从索引的第一个字段开始匹配
  • 高选择性字段放在前面可以更快地缩小结果集
  • 排序字段必须与索引顺序一致才能使用索引排序

Q3: 什么时候应该使用稀疏索引?

A3: 稀疏索引适用于以下场景:

  • 字段只存在于部分文档中
  • 字段包含大量null值
  • 只需要查询包含该字段的文档

Q4: 如何优化包含多个条件的查询?

A4: 优化包含多个条件的查询:

  • 分析查询条件的选择性,选择最具选择性的字段作为索引前缀
  • 遵循ESR原则设计复合索引
  • 考虑使用部分索引过滤掉不需要的文档
  • 使用explain()分析不同索引的性能

Q5: 如何处理大集合的索引创建?

A5: 处理大集合的索引创建:

  • 使用background选项在线创建索引
  • 在低峰期创建索引
  • 监控索引创建过程的资源使用
  • 考虑使用滚动升级方式创建索引

Q6: 索引对写入性能的影响有多大?

A6: 索引对写入性能的影响取决于:

  • 索引数量:每个索引都会增加写入开销
  • 索引大小:较大的索引写入开销更大
  • 文档大小:大型文档的索引写入开销更大
  • 写入频率:高写入频率下索引影响更明显

Q7: 如何监控索引使用情况?

A7: 监控索引使用情况的方法:

  • 使用 $indexStats 聚合查看索引访问统计
  • 分析MongoDB日志中的慢查询
  • 使用监控工具(如Atlas、Prometheus)查看索引命中率
  • 定期运行 db.collection.explain("executionStats") 分析查询

Q8: 什么时候应该重建索引?

A8: 以下情况应该重建索引:

  • 索引碎片化严重
  • 索引大小异常增长
  • 查询性能下降
  • 集合经历了大量的删除操作
  • 升级MongoDB版本后

Q9: 如何优化排序操作?

A9: 优化排序操作:

  • 确保排序字段在索引中
  • 排序方向与索引方向一致
  • 避免在大型结果集上排序
  • 考虑使用覆盖索引

Q10: 覆盖索引的优势是什么?

A10: 覆盖索引的优势:

  • 查询可以完全从索引中获取数据,不需要回表
  • 减少磁盘I/O,提高查询性能
  • 适合频繁执行的聚合查询
javascript
// 覆盖索引示例:查询只需要id和name字段
db.collection.createIndex({ field: 1, id: 1, name: 1 })
db.collection.find({ field: "value" }, { id: 1, name: 1, _id: 0 })