一、为什么你的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)

六、常见误区与注意事项

  1. 索引字段顺序错误:把范围查询字段放在复合索引前面
  2. 过度索引:每个查询都创建独立索引,导致写入性能下降
  3. 忽略索引选择性:为低选择性字段(如性别)创建索引效果差
  4. 忘记定期重建索引:索引碎片化会影响性能
// 重建索引
db.products.reIndex()

七、总结与最佳实践

经过这些优化,我们的商品查询从800ms降到了20ms,效果立竿见影。记住这些最佳实践:

  1. 遵循ESR规则创建复合索引
  2. 使用覆盖索引减少IO
  3. 定期监控和优化索引
  4. 平衡读写性能,避免过度索引

就像给汽车做保养一样,索引也需要定期维护。希望这些经验能帮你告别慢查询,让MongoDB飞起来!