Skip to content

MongoDB 索引维护与监控

索引维护的重要性

  1. 保持索引性能:定期维护可以防止索引性能下降
  2. 减少存储空间:清理索引碎片可以释放存储空间
  3. 提高查询效率:优化的索引可以加速查询操作
  4. 降低写操作开销:高效的索引可以减少写操作的延迟
  5. 预防索引相关故障:提前发现并解决索引问题

索引碎片管理

索引碎片产生的原因

  1. 频繁的插入操作:导致索引页分裂
  2. 频繁的删除操作:留下空的索引空间
  3. 更新操作:可能导致索引键的移动
  4. 不连续的索引键:导致索引页填充不均匀

检测索引碎片

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

// 查看索引的大小和使用情况
const stats = db.collection.stats()
for (let indexName in stats.indexSizes) {
  print(`${indexName}: ${stats.indexSizes[indexName]} bytes`)
}

// 使用 db.currentOp() 查看索引构建情况
db.currentOp({
  $or: [
    { "msg": /Index build/ },
    { "cmd.createIndexes": { $exists: true } }
  ]
})

清理索引碎片

  1. 重建索引
javascript
// 重建单个索引
db.collection.reIndex("index_name")

// 重建所有索引
db.collection.reIndex()
  1. 删除并重建索引
javascript
// 删除索引
db.collection.dropIndex("index_name")

// 重建索引
db.collection.createIndex({ field: 1 }, { name: "index_name" })
  1. 使用 compact 命令
javascript
// 压缩集合(包括索引)
db.runCommand({ compact: "collection_name" })

索引重建最佳实践

  1. 在低峰期进行:避免影响生产环境性能
  2. 逐步重建:不要同时重建所有索引
  3. 监控重建进度:使用 db.currentOp() 查看进度
  4. 备份数据:在重建索引前备份重要数据
  5. 测试性能:重建前后测试查询性能

索引监控

索引使用统计

javascript
// 启用查询分析器
db.setProfilingLevel(1, { slowms: 100 })

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

// 查看全表扫描情况
db.system.profile.find({
  op: "query",
  "executionStats.executionStages.inputStage.stage": "COLLSCAN"
}).limit(10)

索引性能指标

  1. 索引命中率:索引被使用的频率
  2. 索引扫描数量:每次查询扫描的索引条目数
  3. 索引大小:索引占用的存储空间
  4. 索引更新次数:索引被更新的频率
  5. 索引构建时间:创建或重建索引所需的时间

监控工具

  1. MongoDB Atlas:提供索引使用情况的可视化监控
  2. MongoDB Ops Manager:企业版监控工具,支持索引性能监控
  3. Prometheus + Grafana:通过 MongoDB 导出器监控索引指标
  4. Datadog:提供索引性能的监控和告警
  5. New Relic:支持 MongoDB 索引监控

自定义监控脚本

javascript
// 创建索引监控脚本
function monitorIndexes() {
  const collections = db.getCollectionNames()
  
  for (let coll of collections) {
    print(`\n=== 集合 ${coll} 索引监控 ===`)
    const collStats = db.getCollection(coll).stats()
    
    // 查看索引大小
    print("索引大小:")
    for (let indexName in collStats.indexSizes) {
      const sizeMB = collStats.indexSizes[indexName] / (1024 * 1024)
      print(`  ${indexName}: ${sizeMB.toFixed(2)} MB`)
    }
    
    // 查看索引使用情况(需要启用 profiling)
    const indexUsage = db.system.profile.aggregate([
      { $match: { ns: `${db.getName()}.${coll}`, op: "query" } },
      { $group: {
          _id: "$planSummary",
          count: { $sum: 1 },
          avgTime: { $avg: "$millis" }
        }
      },
      { $sort: { count: -1 } }
    ])
    
    print("\n索引使用情况:")
    indexUsage.forEach(usage => {
      print(`  ${usage._id}: ${usage.count} 次查询,平均耗时 ${usage.avgTime.toFixed(2)} ms`)
    })
  }
}

// 运行监控脚本
monitorIndexes()

索引使用分析

识别未使用的索引

javascript
// 查看索引使用统计
db.adminCommand({ serverStatus: 1 }).indexCounters

// 使用 $indexStats 查看索引使用情况
db.collection.aggregate([
  { $indexStats: {} }
])

// 查找未使用的索引(需要启用 profiling)
const indexes = db.collection.getIndexes().map(idx => idx.name)
const usedIndexes = new Set()

// 收集使用过的索引
db.system.profile.find({
  op: { $in: ["query", "getMore"] },
  "executionStats.executionStages.inputStage.stage": "IXSCAN"
}).forEach(doc => {
  const indexName = doc.executionStats.executionStages.inputStage.keyPattern
    ? Object.keys(doc.executionStats.executionStages.inputStage.keyPattern).join("_")
    : ""
  usedIndexes.add(indexName)
})

// 找出未使用的索引
const unusedIndexes = indexes.filter(idx => !usedIndexes.has(idx) && idx !== "_id_")
print("未使用的索引:", unusedIndexes)

识别低效索引

javascript
// 查看低效索引(扫描大量文档但返回少量结果)
db.system.profile.find({
  op: "query",
  "executionStats.executionStages.inputStage.stage": "IXSCAN",
  $expr: {
    $gt: [
      "$executionStats.totalKeysExamined",
      { $multiply: ["$executionStats.nReturned", 10] }
    ]
  }
}).sort({ "executionStats.totalKeysExamined": -1 })

索引优化策略

定期审查索引

  1. 每季度审查一次:检查索引使用情况
  2. 删除未使用的索引:减少存储空间和写操作开销
  3. 合并低效索引:将多个单字段索引合并为复合索引
  4. 优化复合索引顺序:根据查询模式调整字段顺序
  5. 使用部分索引:只对常用文档创建索引

索引性能优化

  1. 调整索引键顺序:将选择性高的字段放在前面
  2. 使用覆盖索引:减少磁盘 I/O
  3. 避免过度索引:每个集合的索引数量建议不超过 64 个
  4. 使用 TTL 索引:自动管理过期数据
  5. 考虑索引压缩:减少索引存储空间

索引创建最佳实践

  1. 在低峰期创建:避免影响生产环境
  2. 使用后台创建:对于大型集合,使用 background: true
  3. 监控创建进度:使用 db.currentOp() 查看索引创建状态
  4. 测试查询性能:创建前后测试查询响应时间
  5. 记录索引变更:记录索引的创建、修改和删除操作

索引相关命令

索引信息查询

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

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

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

// 查看索引使用情况
db.collection.aggregate([{ $indexStats: {} }])

索引管理命令

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

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

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

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

// 压缩集合(包括索引)
db.runCommand({ compact: "collection_name" })

索引分析命令

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

// 查看查询性能统计
db.collection.find({ field: "value" }).explain("allPlansExecution")

// 分析索引使用情况
db.collection.aggregate([
  { $indexStats: {} },
  { $sort: { accesses.ops: -1 } }
])

索引监控工具配置

配置查询分析器

yaml
# mongod.conf
operationProfiling:
  mode: slowOp
  slowOpThresholdMs: 100
  rateLimit: 100

启用索引统计

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

// 查看索引统计
db.serverStatus().indexCounters

使用 Prometheus + Grafana 监控

  1. 安装 MongoDB 导出器
bash
docker run -d -p 9216:9216 -p 27017:27017 --name mongodb-exporter \
  percona/mongodb_exporter:0.20 --mongodb.uri mongodb://localhost:27017
  1. 配置 Prometheus
yaml
scrape_configs:
  - job_name: 'mongodb'
    static_configs:
      - targets: ['mongodb-exporter:9216']
  1. 创建 Grafana 仪表盘
    • 导入 MongoDB 仪表盘模板(ID: 7353)
    • 监控索引大小、索引使用次数、索引命中率等指标

常见索引问题及解决方案

1. 索引过大

问题:索引占用过多存储空间

解决方案

  • 删除未使用的索引
  • 优化复合索引,合并类似索引
  • 使用部分索引减少索引大小
  • 考虑使用索引压缩(企业版)

2. 索引更新缓慢

问题:写操作因索引更新而延迟

解决方案

  • 减少索引数量
  • 优化索引结构
  • 考虑使用更高效的索引类型
  • 增加硬件资源(CPU、内存、磁盘)

3. 索引碎片过多

问题:索引碎片导致查询性能下降

解决方案

  • 重建索引
  • 使用 compact 命令压缩集合
  • 优化写操作模式,减少碎片化

4. 索引不被使用

问题:创建的索引没有被查询使用

解决方案

  • 检查查询模式是否与索引匹配
  • 调整索引键顺序
  • 考虑使用不同的索引类型
  • 删除未使用的索引

5. 索引构建时间过长

问题:创建索引需要很长时间

解决方案

  • 在低峰期创建索引
  • 使用 background: true 选项
  • 考虑使用滚动索引构建
  • 增加硬件资源

索引维护计划

每日维护任务

  1. 监控索引使用情况:查看慢查询日志,识别索引问题
  2. 检查索引构建状态:确保没有长时间运行的索引构建任务
  3. 监控索引大小变化:发现异常增长的索引

每周维护任务

  1. 分析索引使用统计:识别未使用和低效的索引
  2. 检查索引碎片:评估是否需要重建索引
  3. 优化查询计划:根据索引使用情况调整查询

每月维护任务

  1. 重建碎片化严重的索引:提高索引性能
  2. 删除未使用的索引:释放存储空间
  3. 审查索引策略:根据业务变化调整索引设计

季度维护任务

  1. 全面索引审查:评估所有索引的有效性
  2. 更新索引设计:根据查询模式变化调整索引
  3. 测试索引性能:对比优化前后的查询性能

常见问题(FAQ)

Q1: 如何判断是否需要重建索引?

A1: 当出现以下情况时,应该考虑重建索引:

  • 索引碎片率超过 30%
  • 查询性能明显下降
  • 索引大小异常增长
  • 频繁出现索引相关的慢查询
  • 长时间运行的索引构建任务

Q2: 重建索引会影响生产环境吗?

A2: 重建索引会占用系统资源,可能影响生产环境性能。建议:

  • 在低峰期进行索引重建
  • 使用 background: true 选项(对于大型集合)
  • 逐步重建索引,不要同时重建所有索引
  • 监控系统资源使用情况

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

A3: 可以通过以下方式监控索引使用情况:

  • 启用查询分析器,查看慢查询日志
  • 使用 $indexStats 聚合管道查看索引使用统计
  • 使用 db.serverStatus().indexCounters 查看索引计数器
  • 使用第三方监控工具(Prometheus + Grafana、Datadog 等)

Q4: 未使用的索引会影响性能吗?

A4: 是的,未使用的索引会:

  • 占用存储空间
  • 增加写操作的开销(每次写操作都需要更新所有索引)
  • 增加内存使用(索引需要加载到内存中)
  • 影响查询优化器的决策

Q5: 如何优化复合索引的顺序?

A5: 复合索引的顺序应该根据查询模式确定:

  • 首先放置选择性高的字段
  • 其次放置常用于精确匹配的字段
  • 最后放置用于范围查询或排序的字段
  • 考虑查询条件的组合方式

Q6: 索引数量过多会有什么影响?

A6: 索引数量过多会:

  • 增加存储空间占用
  • 增加写操作的延迟
  • 降低写操作的吞吐量
  • 影响查询优化器的性能
  • 增加管理复杂度

Q7: 如何使用部分索引减少索引大小?

A7: 部分索引只包含满足特定条件的文档,可以通过 partialFilterExpression 选项创建:

javascript
db.collection.createIndex(
  { field: 1 },
  { partialFilterExpression: { status: { $eq: "active" } } }
)

Q8: 如何监控索引构建进度?

A8: 可以使用以下方法监控索引构建进度:

javascript
// 查看索引构建状态
db.currentOp({
  $or: [
    { "msg": /Index build/ },
    { "cmd.createIndexes": { $exists: true } }
  ]
})

// 查看索引构建进度(MongoDB 4.2+)
db.collection.aggregate([
  { $listIndexes: {} }
])

Q9: 如何处理索引构建失败?

A9: 索引构建失败的处理方法:

  • 查看错误日志,确定失败原因
  • 检查硬件资源是否充足
  • 尝试在低峰期重新构建
  • 考虑使用更小的批量大小
  • 对于大型集合,考虑使用滚动索引构建

Q10: 如何备份和恢复索引?

A10: 索引通常不需要单独备份,因为:

  • 索引可以从数据中重新构建
  • 备份数据后,可以在恢复时重建索引
  • 对于大型集合,可以考虑备份索引定义,以便快速重建
javascript
// 备份索引定义
const indexes = db.collection.getIndexes()
printjson(indexes)

// 从定义中重建索引
indexes.forEach(idx => {
  if (idx.name !== "_id_") {
    delete idx.v
    delete idx.ns
    delete idx.name
    db.collection.createIndex(idx.key, idx)
  }
})