一、索引为什么会突然罢工?

想象一下你正在图书馆找书,管理员告诉你"所有图书目录卡都乱序存放了"——这就是MongoDB索引失效时的真实写照。最近处理的生产事故就特别典型:某电商平台大促时,商品搜索接口突然响应时间从200ms飙升到8秒,最后发现是组合索引字段顺序出了问题。

先看个活生生的例子(以下示例均使用MongoDB 5.0+语法):

// 问题索引定义
db.products.createIndex({
  category: 1,     // 第一字段
  price: -1,       // 第二字段 
  stock: 1         // 第三字段
})

// 致命查询 - 完全跳过了索引首字段
db.products.find({
  price: {$gt: 100},  // 直接以第二字段作为条件
  stock: {$lt: 50}    // 配合第三字段
}).explain("executionStats")  // 查看执行计划

执行计划会显示"COLLSCAN"这个刺眼的红色警告,意味着进行了全集合扫描。就像你明明知道书在图书馆A区,却非要翻遍整个图书馆。

二、六大常见失效陷阱

1. 最左前缀原则的坑

组合索引就像电话号码的区号,缺少前面数字后面就接不通。比如定义了{A,B,C}索引:

// 有效查询
db.col.find({A:1})                 // ✅ 使用索引
db.col.find({A:1, B:2})            // ✅ 完美匹配
db.col.find({A:1, B:2, C:3})       // ✅ 三字段全用

// 失效查询 
db.col.find({B:2})                 // ❌ 缺少A字段
db.col.find({B:2, C:3})            // ❌ 首字段缺失

2. 类型不匹配的暗礁

MongoDB是类型敏感的,数字5和字符串"5"在索引里完全是两个物种:

// 索引字段是Number类型
db.users.createIndex({age:1})

// 查询时传入字符串
db.users.find({age:"25"})  // ❌ 类型不匹配导致索引失效

3. 运算符的温柔陷阱

不是所有查询运算符都能愉快地使用索引:

// 能使用索引的运算符
db.orders.find({total:{$gt:1000}})   // ✅ 范围查询
db.orders.find({status:"shipped"})   // ✅ 等值查询

// 导致失效的运算符
db.orders.find({tags:{$size:3}})     // ❌ $size无法用索引
db.orders.find({desc:{$regex:"^紧急"}}) // ❌ 左模糊正则

4. 索引选择性过低

给性别字段建索引就像给图书馆所有书贴"文学/非文学"标签——根本筛不出内容:

// 低选择性索引
db.people.createIndex({gender:1})  // 值只有"男"/"女"

// 高选择性索引
db.people.createIndex({id_card:1}) // 身份证号唯一

5. 索引膨胀的恶性循环

当索引大小超过内存时,系统会频繁在磁盘和内存间交换数据:

// 查看索引大小
db.stats(1024*1024)  // 显示MB单位

// 索引内存占用情况
db.serverStatus().mem

6. 隐式转换的代价

MongoDB 4.2+虽然支持模式验证,但旧版本可能自动转换类型:

// 插入混合类型数据
db.log.insert([
  {value: 42},       // Number
  {value: "42"}      // String
])

// 查询时可能意外失效
db.log.find({value:42})  // 部分文档无法命中索引

三、重建索引的实战手册

1. 诊断索引状态

就像医生先要检查才能开药:

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

// 分析查询性能
db.products.find({...}).explain("allPlansExecution")

// 监控慢查询
db.setProfilingLevel(1, 200)  // 记录超过200ms的操作

2. 安全重建流程

直接dropIndex可能在高峰期引发雪崩,推荐分步操作:

// 1. 创建新索引(后台模式)
db.products.createIndex(
  {category:1, brand:1, price:-1},
  {background: true, name: "new_prod_idx"}
)

// 2. 验证新索引生效
db.products.find(
  {category:"电子", brand:"华为"}, 
  {_id:0, name:1, price:1}
).explain()

// 3. 删除旧索引(业务低峰期)
db.products.dropIndex("old_index_name")

3. 特殊场景处理

处理时间序列数据时,TTL索引需要特别注意:

// 原TTL索引
db.logs.createIndex({createTime:1}, {expireAfterSeconds:3600})

// 重建时必须保留TTL属性
db.logs.createIndex(
  {createTime:1}, 
  {
    expireAfterSeconds:3600,
    background: true 
  }
)

四、防患于未然的索引管理

1. 索引设计黄金法则

  • 三星原则:一星等值查询、二星排序、三星覆盖查询
  • 20%法则:索引不应超过文档数量的20%
  • 热分离:将高频查询字段放在索引左侧

2. 自动化监控方案

用这个脚本定期检查索引健康度:

function checkIndexHealth() {
  const colls = db.getCollectionNames();
  colls.forEach(coll => {
    const stats = db[coll].stats();
    const indexSizeMB = stats.totalIndexSize / (1024 * 1024);
    const dataSizeMB = stats.size / (1024 * 1024);
    
    if (indexSizeMB > dataSizeMB * 0.3) {
      print(`警告:${coll}集合索引过大 ${indexSizeMB.toFixed(2)}MB`);
    }
    
    db[coll].getIndexes().forEach(idx => {
      const key = JSON.stringify(idx.key);
      const ops = db[coll].aggregate([
        {$indexStats: {}},
        {$match: {name: idx.name}}
      ]).toArray();
      
      if (ops.length > 0 && ops[0].accesses.ops === 0) {
        print(`闲置索引:${coll}.${idx.name} ${key}`);
      }
    });
  });
}

3. 版本升级注意事项

MongoDB 4.2引入的列存索引与5.0的通配符索引都有特殊要求:

// 通配符索引示例
db.inventory.createIndex({"attributes.$**": 1})

// 使用时必须显式指定字段
db.inventory.find({"attributes.color": "red"})  // ✅
db.inventory.find({"attributes": {size: "XL"}}) // ❌

4. 文档模型的优化配合

适当的文档结构能减少索引压力:

// 反范式化设计示例
// 原始结构
{
  _id: 1,
  product: "手机",
  reviews: [
    {user: "A", score: 5},
    {user: "B", score: 4}
  ]
}

// 优化后结构
{
  _id: 1,
  product: "手机",
  review_count: 2,          // 可索引的聚合字段
  avg_score: 4.5            // 预计算值
}

索引就像数据库的GPS导航,失效时查询就会变成无头苍蝇。通过定期检查执行计划、控制索引数量、遵循最佳实践,才能让查询始终保持高速。记住:好的索引策略不是一劳永逸的,需要像园丁修剪植物一样持续维护。