一、索引为什么会突然罢工?
想象一下你正在图书馆找书,管理员告诉你"所有图书目录卡都乱序存放了"——这就是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导航,失效时查询就会变成无头苍蝇。通过定期检查执行计划、控制索引数量、遵循最佳实践,才能让查询始终保持高速。记住:好的索引策略不是一劳永逸的,需要像园丁修剪植物一样持续维护。
评论