一、为什么索引会失效?

咱们先来聊聊索引失效的常见场景。想象一下,你给图书馆的书都编了目录卡片,结果有人来查书时却不用卡片柜,非要一本本翻书架——这就是典型的索引失效。

在MongoDB中,最常见的失效情况包括:

  1. 查询条件没有匹配索引字段
// 集合有 {name:1} 的索引
db.users.find({age: 25})  // 这个查询完全用不上name索引
  1. 使用了不支持的查询操作符
// 集合有 {price:1} 的索引
db.products.find({price: {$exists: true}})  // $exists操作符无法使用索引
  1. 索引选择性太低
// status字段只有"active"/"inactive"两种值
db.orders.createIndex({status:1})  // 这种低选择性索引效果很差

二、索引失效的典型表现

怎么判断索引是否失效了呢?这里有几个明显的信号:

  1. 查询执行计划显示COLLSCAN
db.orders.find({user:"张三"}).explain()
// 输出中出现"stage":"COLLSCAN"就是全表扫描
  1. 查询性能突然下降
// 原本毫秒级的查询突然变成秒级
db.logs.find({timestamp: {$gt: ISODate("2023-01-01")}})
  1. 内存使用异常增长
// 监控发现大量数据被加载到内存
db.serverStatus().mem

三、索引优化的实战技巧

下面分享几个实用的优化方法,都是我在生产环境验证过的:

  1. 复合索引的黄金法则
// 好的复合索引示例:等值查询字段在前,范围查询在后
db.orders.createIndex({user:1, createDate:-1})

// 查询示例(完美命中索引)
db.orders.find({user:"李四", createDate:{$gt: ISODate("2023-06-01")}})
  1. 覆盖索引的妙用
// 创建包含所有查询字段的索引
db.products.createIndex({category:1, price:1, stock:1})

// 查询只需要索引字段时,无需查文档
db.products.find({category:"电子"}, {price:1, _id:0})
  1. 表达式索引的特殊场景
// 对字段进行大小写不敏感查询
db.users.createIndex({name: 1}, {collation: {locale: "en", strength: 2}})

// 使用相同的collation查询
db.users.find({name:"john"}).collation({locale:"en", strength:2})

四、高级优化策略

对于复杂场景,我们需要更高级的技巧:

  1. 部分索引节省空间
// 只为活跃用户创建索引
db.customers.createIndex(
  {email:1},
  {partialFilterExpression: {status:"active"}}
)
  1. TTL索引自动清理
// 自动删除30天前的日志
db.applogs.createIndex(
  {createdAt:1},
  {expireAfterSeconds: 2592000}
)
  1. 索引交集优化
// 当查询条件涉及多个独立索引时
db.events.createIndex({type:1})
db.events.createIndex({timestamp:-1})

// 查询可能使用索引交集
db.events.find({type:"login", timestamp:{$gt: ISODate("2023-07-01")}})

五、性能监控与维护

索引不是创建完就完事了,还需要持续维护:

  1. 使用索引统计信息
// 查看索引使用情况
db.orders.aggregate([{$indexStats:{}}])

// 输出示例:
{
  "name" : "user_1_createDate_-1",
  "accesses" : {
    "ops" : NumberLong(25431),  // 使用次数
    "since" : ISODate("2023-01-01T00:00:00Z")
  }
}
  1. 定期重建碎片化索引
// 重建单个索引
db.orders.reIndex({index:"user_1"})

// 重建集合所有索引
db.orders.reIndex()
  1. 使用性能分析工具
// 开启查询分析
db.setProfilingLevel(2, 50)  // 记录所有超过50ms的查询

// 查看慢查询
db.system.profile.find().sort({millis:-1}).limit(10)

六、实际案例分析

来看一个真实的优化案例。某电商平台的订单查询接口突然变慢:

优化前查询:

db.orders.find({
  userId: ObjectId("5f8d..."),
  status: {$in: ["paid","shipped"]},
  createTime: {$gt: ISODate("2023-01-01")}
}).sort({updateTime:-1})

问题分析:

  1. 现有索引是{userId:1}
  2. $in操作导致索引效率降低
  3. 排序字段没有索引支持

优化方案:

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

// 优化后查询(使用hint强制使用新索引)
db.orders.find({
  userId: ObjectId("5f8d..."),
  status: {$in: ["paid","shipped"]},
  createTime: {$gt: ISODate("2023-01-01")}
}).sort({updateTime:-1}).hint("userId_1_status_1_createTime_1_updateTime_-1")

优化效果:

  • 查询时间从1200ms降至80ms
  • 扫描文档数从15万降至200
  • 内存使用减少60%

七、常见误区与注意事项

在索引优化过程中,有几个容易踩的坑:

  1. 索引不是越多越好
// 每个插入/更新操作都需要维护所有相关索引
db.products.insert({
  name: "手机",
  price: 3999,
  category: "电子产品",
  tags: ["智能","5G"],
  specs: {ram:"8GB", storage:"256GB"}
})
// 如果有10个索引,这个插入就要更新10个索引结构
  1. 避免过度使用数组索引
// 数组字段索引会为每个数组元素创建索引条目
db.products.createIndex({tags:1})
// 如果tags数组平均有5个元素,索引大小会膨胀5倍
  1. 注意索引大小限制
// MongoDB单个索引条目不能超过1024字节
db.logs.createIndex({content:1})  // 如果content字段很大,可能无法创建

八、总结与最佳实践

经过上面的分析,我们可以总结出一些MongoDB索引优化的黄金法则:

  1. 遵循ESR原则(Equality, Sort, Range)创建复合索引
  2. 优先考虑高选择性的字段建立索引
  3. 定期使用explain()分析查询执行计划
  4. 监控索引使用情况,及时清理无用索引
  5. 对于大型集合,考虑使用分片配合索引优化

记住,索引优化是一个持续的过程。随着数据增长和查询模式变化,需要不断调整索引策略。最好的做法是建立完善的监控机制,在性能问题出现前就能发现潜在风险。

最后分享一个实用的索引检查清单:

  1. 查询是否使用了预期索引?
  2. 索引是否覆盖了查询所需字段?
  3. 排序操作是否有索引支持?
  4. 索引的选择性是否足够高?
  5. 索引大小是否在合理范围内?