MongoDB出现大量慢查询怎么建索引
摘要:# MongoDB慢查询卡成狗?别慌,建索引就对了,但别瞎建! 我前两天帮一个做电商的朋友看后台,好家伙,一个商品列表页加载要十几秒。他愁眉苦脸地说:“加了高防,上了CDN,源站带宽也够,怎么还是这么慢?” 我一查数据库日志,满屏的慢查询,全在扫全表(C…
MongoDB慢查询卡成狗?别慌,建索引就对了,但别瞎建!
我前两天帮一个做电商的朋友看后台,好家伙,一个商品列表页加载要十几秒。他愁眉苦脸地说:“加了高防,上了CDN,源站带宽也够,怎么还是这么慢?” 我一查数据库日志,满屏的慢查询,全在扫全表(COLLSCAN)。问题根本不在防护上,而在数据库里——他那MongoDB的集合,压根没建对索引。
这种感觉你懂吧?钱花在刀背上,最该优化的地方却裸奔着。
很多团队一提到数据库优化,就想到加内存、升配置。其实吧,绝大多数MongoDB性能问题,根源就俩字:索引。 或者更准确地说,是缺索引,或者建了没用的索引。今天咱就抛开那些晦涩的官方文档,用大白话聊聊,当MongoDB慢查询多到让你头皮发麻时,到底该怎么建索引。
第一步:先别急着动手,把“元凶”揪出来
很多人的第一反应是:“慢?那我赶紧建几个索引试试。”——打住!这跟生病了乱吃药一个道理。
你得先知道是哪个查询在慢。MongoDB自带的数据库分析器(Profiler) 就是你的“诊断仪”。把它打开(级别设为1,记录慢查询就行),跑一会儿业务。
然后,重点看那些planSummary: "COLLSCAN"的记录。这意思就是:“老弟,你这查询没走索引,我把整个表从头到尾翻了一遍才找到你要的数据。” 对于动辄百万、千万级的集合,这能不慢吗?
关键一步来了: 别只看一条,把同一种查询模式的慢日志多收集几条。你要找的是最频繁出现、且最耗时的查询模式。比如,是不是所有按userId和createTime范围查询的订单都慢?这才是你要下刀子的地方。
第二步:建索引的核心心法: ESR 原则
索引不是越多越好。每多一个索引,写操作(增、删、改)就会慢一点,因为数据库要额外维护索引树。所以,怎么用最少的索引,解决最多的问题?
记住这个ESR原则(Equality, Sort, Range),这是MongoDB官方推荐的索引构建最佳实践,说白了就是按优先级排字段:
- E (等值查询字段) 放最前面:比如
status: "active",category: "book"。这些字段能快速把数据范围缩小到一个很小的子集。 - S (排序字段) 放中间:比如
ORDER BY createTime DESC。如果排序字段能被索引覆盖,数据库就不用做费内存的临时排序了。 - R (范围查询字段) 放最后:比如
createTime: {$gt: ISODate("...")},age: {$lt: 30}。范围查询本身就会扫描索引的一部分,放最后效率最高。
举个例子: 你最常见的慢查询是:“查找某个用户(userId=xxx)在过去一个月(createTime > 某时间)的已完成(status="completed")订单,并按创建时间倒序(sort by createTime DESC)返回。”
根据ESR原则,最优的复合索引应该是:{ userId: 1, status: 1, createTime: -1 }
userId和status是等值查询(E)。createTime既用于范围查询(R),又用于排序(S)。因为我们按降序排,所以索引里也设为降序(-1),这样数据库就可以顺着索引读,效率最高。
很多所谓的高端优化方案,PPT很猛,真到建索引时连这个基础原则都忘了,你说能不出问题吗?
第三步:避开这些坑,你的索引才算没白建
- 别在低基数字段上单建索引。什么是低基数?比如
gender(就男/女),status(就几种状态)。这种字段区分度太差,走索引和全表扫描可能差不了多少,还白占资源。它只适合在复合索引里作为前缀(E部分)使用,用来快速过滤掉大部分数据。 - 小心
$or查询。{$or: [{a:1}, {b:2}]}这种查询,MongoDB可能会使用a和b各自的独立索引,然后合并结果,但效率往往不如人意。如果$or频繁出现且慢,考虑能否用$in重构,或者……认命地建两个复合索引分别覆盖两种情况。 - 覆盖查询(Covered Query)是“神技”。如果你的查询只需要返回索引中包含的字段,MongoDB可以直接从索引里取数据,根本不用去碰真实的文档数据(回表),速度飞起。在
explain()结果里看到"stage": "PROJECTION_COVERED"或"indexOnly": true,你就偷着乐吧。 - 定期用
$indexStats看看索引“热度”。有些索引建了半年,一次都没被用过(accesses.ops为0),这就是纯粹的累赘,果断删掉。数据库也需要“断舍离”。
第四步:实战操作与验证
假设我们锁定了那个订单查询,现在要建索引 { userId: 1, status: 1, createTime: -1 }。
// 1. 创建索引
db.orders.createIndex({ userId: 1, status: 1, createTime: -1 })
// 2. 用explain验证是否走索引
db.orders.find({
userId: "user123",
status: "completed",
createTime: { $gt: new Date("2023-10-01") }
}).sort({ createTime: -1 }).explain("executionStats")
重点看输出里的:
winningPlan.inputStage.stage: 如果是IXSCAN(索引扫描),恭喜你,索引生效了。executionStats.totalDocsExamined: 检查的文档数。这个数应该远小于集合总数,如果还是很大,说明索引过滤性不够好。executionStats.executionTimeMillis: 执行时间,建索引后应该大幅下降。
最后说点大实话
数据库优化,尤其是索引,是个持续的过程,不是一劳永逸的。业务在变,查询模式也在变。你今天建好的最优索引,下个版本可能就成了拖累。
我的习惯是: 把慢查询监控做成常态化,每周扫一眼。新的慢查询冒头了,就重复上面的“诊断-分析-建索引-验证”流程。
说到底,技术方案最怕“配错”。高防IP再牛,防不住数据库自己慢;服务器配置再高,也扛不住全表扫描。把资源花在刀刃上,从一条正确的索引开始,往往比加钱升配来得实在。
行了,不废话了,赶紧去db.currentOp()看看有没有正在折磨你的慢查询吧。

