一、为什么索引会失效?
咱们先来聊聊索引失效的常见场景。想象一下,你给图书馆的书都编了目录卡片,结果有人来查书时却不用卡片柜,非要一本本翻书架——这就是典型的索引失效。
在MongoDB中,最常见的失效情况包括:
- 查询条件没有匹配索引字段
// 集合有 {name:1} 的索引
db.users.find({age: 25}) // 这个查询完全用不上name索引
- 使用了不支持的查询操作符
// 集合有 {price:1} 的索引
db.products.find({price: {$exists: true}}) // $exists操作符无法使用索引
- 索引选择性太低
// status字段只有"active"/"inactive"两种值
db.orders.createIndex({status:1}) // 这种低选择性索引效果很差
二、索引失效的典型表现
怎么判断索引是否失效了呢?这里有几个明显的信号:
- 查询执行计划显示COLLSCAN
db.orders.find({user:"张三"}).explain()
// 输出中出现"stage":"COLLSCAN"就是全表扫描
- 查询性能突然下降
// 原本毫秒级的查询突然变成秒级
db.logs.find({timestamp: {$gt: ISODate("2023-01-01")}})
- 内存使用异常增长
// 监控发现大量数据被加载到内存
db.serverStatus().mem
三、索引优化的实战技巧
下面分享几个实用的优化方法,都是我在生产环境验证过的:
- 复合索引的黄金法则
// 好的复合索引示例:等值查询字段在前,范围查询在后
db.orders.createIndex({user:1, createDate:-1})
// 查询示例(完美命中索引)
db.orders.find({user:"李四", createDate:{$gt: ISODate("2023-06-01")}})
- 覆盖索引的妙用
// 创建包含所有查询字段的索引
db.products.createIndex({category:1, price:1, stock:1})
// 查询只需要索引字段时,无需查文档
db.products.find({category:"电子"}, {price:1, _id:0})
- 表达式索引的特殊场景
// 对字段进行大小写不敏感查询
db.users.createIndex({name: 1}, {collation: {locale: "en", strength: 2}})
// 使用相同的collation查询
db.users.find({name:"john"}).collation({locale:"en", strength:2})
四、高级优化策略
对于复杂场景,我们需要更高级的技巧:
- 部分索引节省空间
// 只为活跃用户创建索引
db.customers.createIndex(
{email:1},
{partialFilterExpression: {status:"active"}}
)
- TTL索引自动清理
// 自动删除30天前的日志
db.applogs.createIndex(
{createdAt:1},
{expireAfterSeconds: 2592000}
)
- 索引交集优化
// 当查询条件涉及多个独立索引时
db.events.createIndex({type:1})
db.events.createIndex({timestamp:-1})
// 查询可能使用索引交集
db.events.find({type:"login", timestamp:{$gt: ISODate("2023-07-01")}})
五、性能监控与维护
索引不是创建完就完事了,还需要持续维护:
- 使用索引统计信息
// 查看索引使用情况
db.orders.aggregate([{$indexStats:{}}])
// 输出示例:
{
"name" : "user_1_createDate_-1",
"accesses" : {
"ops" : NumberLong(25431), // 使用次数
"since" : ISODate("2023-01-01T00:00:00Z")
}
}
- 定期重建碎片化索引
// 重建单个索引
db.orders.reIndex({index:"user_1"})
// 重建集合所有索引
db.orders.reIndex()
- 使用性能分析工具
// 开启查询分析
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})
问题分析:
- 现有索引是{userId:1}
- $in操作导致索引效率降低
- 排序字段没有索引支持
优化方案:
// 创建新复合索引
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%
七、常见误区与注意事项
在索引优化过程中,有几个容易踩的坑:
- 索引不是越多越好
// 每个插入/更新操作都需要维护所有相关索引
db.products.insert({
name: "手机",
price: 3999,
category: "电子产品",
tags: ["智能","5G"],
specs: {ram:"8GB", storage:"256GB"}
})
// 如果有10个索引,这个插入就要更新10个索引结构
- 避免过度使用数组索引
// 数组字段索引会为每个数组元素创建索引条目
db.products.createIndex({tags:1})
// 如果tags数组平均有5个元素,索引大小会膨胀5倍
- 注意索引大小限制
// MongoDB单个索引条目不能超过1024字节
db.logs.createIndex({content:1}) // 如果content字段很大,可能无法创建
八、总结与最佳实践
经过上面的分析,我们可以总结出一些MongoDB索引优化的黄金法则:
- 遵循ESR原则(Equality, Sort, Range)创建复合索引
- 优先考虑高选择性的字段建立索引
- 定期使用explain()分析查询执行计划
- 监控索引使用情况,及时清理无用索引
- 对于大型集合,考虑使用分片配合索引优化
记住,索引优化是一个持续的过程。随着数据增长和查询模式变化,需要不断调整索引策略。最好的做法是建立完善的监控机制,在性能问题出现前就能发现潜在风险。
最后分享一个实用的索引检查清单:
- 查询是否使用了预期索引?
- 索引是否覆盖了查询所需字段?
- 排序操作是否有索引支持?
- 索引的选择性是否足够高?
- 索引大小是否在合理范围内?
评论