一、为什么你的MongoDB查询像蜗牛爬?
相信很多开发同学都遇到过这样的场景:明明数据量不大,MongoDB的查询却慢得让人抓狂。这就像在高速公路上开拖拉机,看着别人嗖嗖超车,自己却只能干着急。其实,90%的慢查询问题都出在索引上。
举个例子,我们有个电商系统的商品集合,存储了100万条商品数据。现在要查询价格在100-200元之间的手机类商品:
// MongoDB查询示例(技术栈:MongoDB 4.4)
db.products.find({
category: "手机",
price: { $gte: 100, $lte: 200 }
}).explain("executionStats") // 查看执行计划
执行后发现扫描了全部100万条文档,耗时800ms。这就像在图书馆找书却不用目录,非要一本本翻,能不慢吗?
二、索引优化的黄金法则
1. 创建合适的单字段索引
最简单的优化就是为常用查询字段创建索引:
// 为category和price字段创建单字段索引
db.products.createIndex({ category: 1 })
db.products.createIndex({ price: 1 })
// 再次执行相同查询
db.products.find({
category: "手机",
price: { $gte: 100, $lte: 200 }
}).explain("executionStats")
现在查询可能使用其中一个索引,但效果有限。MongoDB默认每次查询只能使用一个索引(除非使用索引交集)。
2. 复合索引的正确打开方式
复合索引才是解决多条件查询的利器:
// 创建category和price的复合索引
db.products.createIndex({ category: 1, price: 1 })
// 查询优化后仅扫描1000条文档,耗时50ms
这里有个重要原则:ESR规则(Equality, Sort, Range)。把等值查询字段放最前面,然后是排序字段,最后是范围查询字段。
3. 多键索引处理数组字段
如果商品有标签数组字段,查询特定标签的商品:
// 错误做法:全表扫描
db.products.find({ tags: "促销" })
// 正确做法:创建多键索引
db.products.createIndex({ tags: 1 })
三、高级优化技巧
1. 覆盖索引的魔法
当查询只需要索引字段时,可以完全不访问文档:
// 创建覆盖索引
db.products.createIndex({ category: 1, price: 1, name: 1 })
// 查询只需要索引字段
db.products.find(
{ category: "手机", price: { $gte: 100 } },
{ _id: 0, category: 1, price: 1, name: 1 }
)
2. 部分索引节省空间
只为部分文档创建索引:
// 只为价格>100的商品创建索引
db.products.createIndex(
{ price: 1 },
{ partialFilterExpression: { price: { $gt: 100 } } }
)
3. 索引的隐藏成本
索引不是越多越好,每个写操作都要更新索引:
// 查看索引大小
db.products.totalIndexSize()
// 删除不必要索引
db.products.dropIndex("price_1_category_1")
四、实战案例分析
场景1:分页查询优化
电商首页需要分页展示商品:
// 低效做法
db.products.find()
.sort({ createTime: -1 })
.skip(1000)
.limit(20) // 需要先扫描1020条记录
// 优化方案:使用范围查询代替skip
let lastTime = ... // 获取上一页最后一条记录的createTime
db.products.find({ createTime: { $lt: lastTime } })
.sort({ createTime: -1 })
.limit(20)
场景2:多条件排序
需要按多个字段排序时:
// 低效做法
db.products.find({ category: "手机" })
.sort({ price: 1, sales: -1 }) // 可能导致内存排序
// 优化方案:创建复合索引
db.products.createIndex({ category: 1, price: 1, sales: -1 })
五、性能监控与维护
1. 使用explain分析查询
// 查看查询执行计划
db.products.find(...).explain("allPlansExecution")
重点关注:
- totalDocsExamined:扫描文档数
- executionTimeMillis:执行时间
- stage:查询阶段(COLLSCAN最差,IXSCAN最佳)
2. 慢查询日志
// 启用慢查询日志
db.setProfilingLevel(1, { slowms: 50 })
// 查看慢查询
db.system.profile.find().sort({ ts: -1 }).limit(10)
六、常见误区与注意事项
- 索引字段顺序错误:把范围查询字段放在复合索引前面
- 过度索引:每个查询都创建独立索引,导致写入性能下降
- 忽略索引选择性:为低选择性字段(如性别)创建索引效果差
- 忘记定期重建索引:索引碎片化会影响性能
// 重建索引
db.products.reIndex()
七、总结与最佳实践
经过这些优化,我们的商品查询从800ms降到了20ms,效果立竿见影。记住这些最佳实践:
- 遵循ESR规则创建复合索引
- 使用覆盖索引减少IO
- 定期监控和优化索引
- 平衡读写性能,避免过度索引
就像给汽车做保养一样,索引也需要定期维护。希望这些经验能帮你告别慢查询,让MongoDB飞起来!
评论