MongoDB查询慢怎么办?索引优化实战指南
MongoDB用久了,很多人会遇到查询越来越慢的问题:明明数据量不大,接口响应却要好几秒,数据库CPU还经常跑满。大部分情况下,根源就是索引没建对。
本文从查询分析、索引类型、实战优化案例三个层面,帮你系统性解决MongoDB的性能问题。
如何定位查询慢的原因?
优化之前先要找到问题根源,MongoDB提供了几个排查工具:
| 排查工具 | 用途 | 使用方法 |
|---|---|---|
| explain() | 查看查询执行计划 | db.collection.find(query).explain("executionStats") |
| 慢查询日志 | 记录执行时间超过阈值的查询 | db.setProfilingLevel(1, {slowms: 100}) |
| currentOp() | 查看当前正在执行的操作 | db.currentOp(),用于定位卡住的查询 |
| serverStatus() | 查看数据库整体状态 | 检查连接数、内存使用、锁等待情况 |
用explain()查看查询计划时,重点关注两个数字:totalDocsExamined(扫描的文档数)和nReturned(实际返回的文档数)。如果扫描数远大于返回数(比如扫描10万条只返回10条),说明缺少合适的索引,查询在做全表扫描。
索引类型怎么选?
MongoDB支持多种索引类型,选对类型才能发挥最大效果:
| 索引类型 | 适用场景 | 创建方式 |
|---|---|---|
| 单字段索引 | 按单一字段查询/排序(如按创建时间查询) | db.collection.createIndex({field: 1}) |
| 复合索引 | 多字段联合查询(如按用户ID+状态查询) | db.collection.createIndex({userId: 1, status: 1}) |
| 多键索引 | 数组字段查询(如商品标签tags数组) | 数组字段自动创建,无需特殊语法 |
| 文本索引 | 全文搜索(如商品名称模糊搜索) | db.collection.createIndex({title: "text"}) |
| TTL索引 | 自动过期数据(如临时验证码、session) | db.collection.createIndex({createdAt: 1}, {expireAfterSeconds: 3600}) |
- Equality(等值):精确匹配的字段放最前面,如status: "active"
- Sort(排序):用于排序的字段放中间,如createdAt: -1
- Range(范围):范围查询的字段放最后,如price: {$gt: 100}
不要给每个字段都建索引。索引会占用额外存储空间,且写入时需要同步更新索引,索引越多写入越慢。一般来说,一个集合的索引数量控制在5-8个以内比较合理,优先给高频查询字段建索引。
实战优化案例
以下是几个常见的性能问题和优化方法:
- 案例1 - 订单列表查询慢:查询条件是userId+status,且按createdAt排序。原本只有userId的单字段索引,扫描量大。优化:建立复合索引{userId:1, status:1, createdAt:-1},查询时间从800ms降到5ms
- 案例2 - 模糊搜索商品名称慢:用正则表达式查询商品名,全表扫描。优化:改用文本索引{title:"text"},配合$text查询,速度提升20倍以上
- 案例3 - 分页查询越往后越慢:用skip()+limit()做分页,页数越大跳过的文档越多。优化:改用基于游标的分页,用上次结果的_id作为查询条件(如_id:{$gt:lastId}),避免skip的性能损耗
- 案例4 - 写入性能下降:发现集合有15个索引,每次写入都要更新15个索引结构。优化:用db.collection.aggregate([{$indexStats:{}}])查看索引使用率,删除长期未被使用的索引,减少到6个核心索引
1. 用explain()检查所有慢查询是否命中索引
2. 复合索引遵循ESR规则排列字段顺序
3. 定期用$indexStats检查索引使用率,删除无用索引
4. 避免在查询中使用$where、正则表达式前缀不固定的模糊匹配
5. 大表分页用游标方式,避免skip()性能损耗
索引优化后还是慢?检查这些方面
如果索引已经优化到位,但性能依然不理想,可能是以下问题:
🔍 需要排查的方向
- 规格不足:内存不够导致索引无法完全放入内存,频繁磁盘IO
- 连接池配置:应用连接数配置不合理,导致连接等待
- Schema设计:单个文档过大(超过1MB)或嵌套层级过深,影响读写效率
- 分片需求:单表数据量超过千万级,可能需要考虑分片集群
⚠️ 常见性能陷阱
- 在事务中处理大批量数据,导致长时间锁定
- 频繁的count()操作,大表count非常慢,应该用估算值或缓存结果
- 未设置合理的写关注级别(write concern),过高的一致性要求会拖慢写入
- 副本集读写分离没配置,所有请求都打到主节点
- 内存要求:建议内存能容纳全部索引数据(用db.stats()查看索引总大小)
- 规格选择:2核4G起步,热点数据量大建议4核8G以上
- 架构建议:读多写少场景用副本集+只读节点分担查询压力
常见问题
创建索引会影响线上业务吗?
默认情况下,createIndex()是前台构建,会阻塞其他读写操作,大表上创建索引可能导致长时间锁表。生产环境建议使用后台构建:db.collection.createIndex({field: 1}, {background: true})
后台构建不会阻塞其他操作,但构建时间会更长,且构建期间可能有额外的CPU和IO消耗,建议在业务低峰期执行。MongoDB 4.2以上版本已经优化了索引构建机制,影响更小。
怎么知道哪些索引从来没被用到?
使用聚合命令查看索引使用统计:db.collection.aggregate([{$indexStats: {}}])
返回结果中的accesses.ops字段显示该索引被使用的次数。如果长期观察(建议至少1周,覆盖完整业务周期)发现某个索引ops一直是0,说明这个索引没有被任何查询用到,可以考虑删除,减少写入开销和存储占用。
复合索引建了之后,单字段查询还能用到吗?
取决于查询的字段是否是复合索引的前缀。假设建了索引{a:1, b:1, c:1}:
• 查询{a: x} → ✅ 能用到索引(前缀匹配)
• 查询{a: x, b: y} → ✅ 能用到索引
• 查询{b: y} → ❌ 用不到(缺少前缀a)
• 查询{a: x, c: z} → ⚠️ 只能用到a的部分,c需要额外过滤
这就是为什么设计复合索引时字段顺序很重要,需要把最常用于查询的字段放在前面。
总结
MongoDB查询慢,90%以上的情况都是索引问题,核心优化思路:
第一步:用explain()定位是否走了全表扫描(totalDocsExamined远大于nReturned)
第二步:根据查询模式建立合适的索引,复合索引遵循ESR规则(等值-排序-范围)排列字段顺序
第三步:定期用$indexStats检查索引使用率,清理无用索引,避免索引过多拖慢写入性能
如果索引优化后性能依然不理想,再考虑规格升级、读写分离或分片集群等架构层面的方案。避免一开始就上复杂架构,先把基础的索引优化做到位,往往就能解决大部分性能问题。