从两年前开始,我只要一听到“MongoDB 查询慢”这句话,第一反应已经不是“加索引”了。最初接手那套订单系统时,业务量一上来,一个列表页接口从几百毫秒直接飙到四五秒,DBA 让我看日志,满屏都是 COLLSCAN。后来排查的坑踩得多了,才发现慢查询这件事,十个里有八个根本不是单靠加索引能解决的:有的索引建了但没生效,有的是查询写法把索引废掉了,有的是数据分布把缓存命中了打穿,也有的纯粹是硬件等待把时间吞掉了。这篇就从我实际排查过的案例出发,把 Mongo 查询慢的常见原因、判断方法、排查工具和避坑经验系统地捋一遍,适合正在被慢查询困扰的开发者、DBA,以及刚上手 Mongo 想少走弯路的人。
1. 从一条4.7秒的查询开始:现象、误判与第一反应
1.1 第一现场:一条经营报表接口的慢查询
某天下午,业务方反馈后台经营报表页面加载非常慢,点一次要等好几秒。我先抓了慢日志,定位到一条看起来非常普通的查询:
javascript复制db.orders.find({ status: "PAID", amount: { $gt: 1000 } })
.sort({ createTime: -1 })
.limit(10)
这条语句本身没有任何“高级”写法,没有正则、没有 $where,按常理来说,一条分页查询不至于慢到这个程度。真正让它从普通查询变成慢查询的原因,藏在执行计划里:COLLSCAN,也就是全表扫描。我当时一看 docsExamined: 7345210,只返回了 10 条,却扫了 700 多万条文档,不慢才奇怪。
1.2 第一时间应该问的三个问题
很多人拿到一条慢查询,第一反应是马上写索引、改代码,但这样往往容易漏掉真正的问题。我在排查时习惯先问自己三个问题:这条查询是偶发还是持续性的?是只有这个接口慢,还是整个实例都慢?慢的时候实例的 CPU、磁盘、缓存是什么状态?
这三个问题决定了排查方向。持续性的慢查询,大概率是查询计划、索引或数据分布的问题;偶发性的慢查询,往往和资源竞争、锁等待、磁盘 IO 抖动有关。整个实例都慢,说明问题不在某一条 SQL 上,而是环境层面的瓶颈;只有个别请求慢,那才值得逐条去挖执行计划。很多人在第一步就跳进去了,结果索引建了一堆,慢查询依然存在,就是因为没有先判断慢的性质。
1.3 慢查询的本质:做了太多“无用功”
从数据库的角度看,慢查询的本质就一句话:为了拿到很少的结果,却被逼着看了大量不相关的数据。无论哪个数据库,这个底层逻辑都一样。MySQL 里这叫全表扫描,Mongo 里叫 COLLSCAN;索引扫描和回表的代价,在 Mongo 里对应的是 IXSCAN 加上 FETCH。只要理解了“扫描量”和“返回量”的巨大落差,慢查询的原因就已经找到了一半。
所以整个排查思路都很明确:想办法减少扫描量,让查询路径尽可能短。具体怎么发现扫描量,怎么缩短路径,就是接下来几节要展开的核心内容。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引没那么神奇:COLLSCAN背后的代价与索引失效案例
2.1 为什么全表扫描会这么慢
MongoDB 的默认存储引擎是 WiredTiger,数据以 B+ 树的形式组织在磁盘上。没有索引的时候,find 查询只能从集合的第一个文档开始,逐个读入内存做条件匹配,直到把整个集合看完。集合里有 700 万条文档,它就读 700 万条,一条都不会少。更麻烦的是,Mongo 文档是变长的,并不是按紧凑的定长行存储,扫描过程中还要解析 BSON,这里面的 CPU 开销比很多人想象的大得多。
这里有个特别容易忽略的细节:COLLSCAN 虽然把全表都看了一遍,但 totalKeysExamined 是 0。因为根本没走索引,自然没有 key 可检查。很多初学者会把 keysExamined 和 docsExamined 搞混,其实前者是索引条目的扫描数,后者是文档的扫描数。判断一个查询是否高效,核心就是看这两个数字与返回结果数量之间的差距。
2.2 复合索引的字段顺序:等值条件放前面
我给当时的场景建的索引是:
javascript复制db.orders.createIndex({ status: 1, createTime: -1, amount: 1 })
为什么这么建,而不是 { amount: 1, status: 1, createTime: -1 }?这里涉及复合索引最核心的一个原则:
