MongoDB索引对查询性能的影响:从原理到explain实战优化 MongoDB 索引对查询性能的影响说实话是个老生常谈却又特别容易被忽视的话题。很多人对索引的印象停留在“加索引能让查询变快”但到底快在哪、慢的时候慢在哪、为什么有时候加了索引反而更糟能讲清楚的人并不多。这篇文章我就围绕 MongoDB 索引最核心的几个场景展开结合我实际跑过的查询、踩过的坑、还有用 explain 排查问题的经历把索引对查询性能的影响从头到尾拆一遍。内容不挑基础刚入门的朋友可以当索引扫盲已经写过不少查询的老手也能从里面找到一些平时不太注意的细节。1. 索引到底是什么它凭什么能提速1.1 没有索引时 MongoDB 怎么找数据理解索引对性能的影响得先明白没有索引的时候一个查询是怎么执行的。假设你有一个用户表里面存了几百万条文档你现在要查所有 age 等于 30 的人。没有索引的情况下MongoDB 只能做一个集合扫描Collection Scan从集合里的第一条文档开始一条一条往下读每读一条就要比对一次 age 字段看它是不是等于 30直到把整个集合全扫完。这个过程听起来好像没什么大问题但数据量一旦上来差距就非常恐怖。一条文档如果平均 1KB100 万条就是大概 1GB 的数据就算这些数据都在内存里也要逐个比对CPU 和 IO 的消耗都不小。如果文档更大或者数据已经部分落到磁盘上那一次全表扫描可能要几十秒甚至更久。而且这种扫描是线性的数据量翻倍扫描时间基本也翻倍没有任何办法能绕过去。这时候你可能会想MongoDB 不是自带查询优化器吗它不会自动帮我做点什么吗确实MongoDB 有一个查询优化器它会为每个查询计划估算执行成本然后选一个它认为最合理的执行方案。但当集合里没有任何索引可用时优化器不管怎么选最后都只能落到集合扫描上没有别的路可以走。优化器能做的是矮子里面拔高个而不是无中生有变出一个索引来。1.2 索引是如何改变查询路径的加了索引之后情况就完全不同了。索引在底层是一棵 B 树B-Tree它把某个字段或某几个字段的值按照一定的顺序组织起来每个索引条目还额外保存了指向原始文档的位置信息。这就像你在读一本很厚的书如果没有目录你只能一页页翻有了目录之后你可以直接翻到对应页码效率当然不是一个量级。具体到执行层面查询优化器发现存在能匹配查询条件的索引时会先走索引扫描Index Scan。比如查 age 30它会去索引 B 树里定位到所有 age 为 30 的索引条目然后根据这些条目里保存的位置信息把对应的文档取出来返回给你。因为索引本身就是有序且经过结构化的所以定位一批具体值只需要沿着树往下走访问的节点数量非常有限跟全表扫描的 IO 量完全没有可比性。这里我特别想强调一点索引不只是省了“比对次数”更关键的是省了“读取数据量”。集合扫描必须把文档本身从存储引擎里读出来只有读到文档才知道它的字段值是什么。而索引扫描只需要访问索引页索引页里的数据是精简过的键值对整体体积比文档小得多所以一次查询读入内存的数据量就少这也是索引能明显降低响应时间的根本原因。1.3 查询优化器和索引选择的基本流程MongoDB 的查询优化器并不是拿到查询条件就直接盲目选一个索引它有一个相对固定的流程。第一次执行某个形状的查询时优化器会尝试多个候选计划并行跑一小段看看每个计划的执行成本然后选一个成本最低的作为最终计划把它缓存起来。之后一段时间内相同形状的查询就直接复用这个计划不再重新评估。这个机制带来的一个实际影响是如果你建了一个新索引它未必会立刻替换掉已有的查询计划。因为缓存计划还在优化器不会每次查询都去重新竞争。我见过有人给集合新增了一个明显更好的索引但线上查询时间没有任何变化排查了半天发现是查询计划缓存还挂着旧索引。这种情况最直接的解决办法是清掉相关集合的查询计划缓存或者用 hint 强制走新索引验证效果确认没问题后再让优化器慢慢切换。另外优化器选择索引时并不是以“哪个索引名字更贴切”为标准而是靠估算出来的代价。对于能精确匹配等值条件的索引代价通常很低对于只能做范围扫描的索引代价会高一些如果索引选择性太差比如一个字段总共只有两个值分布又非常不均匀优化器甚至可能觉得走索引不如直接全表扫描来得痛快。所以索引能否真正提升性能得看实际执行计划不能光看“有没有索引”。2. 索引设计选型动手建索引之前要想清楚的事2.1 单字段索引的适用场景与建法最基础的索引就是单字段索引语法非常简单一条命令就能建出来db.users.createIndex({ age: 1 })这条命令的意思是给 users 集合的 age 字段建一个升序索引。建完之后所有针对 age 字段的等值查询、范围查询、还有按 age 排序的操作都有可能用上这个索引。单字段索引最大的优点就是简单直接查询条件里只要包含这个字段优化器就有机会走索引。但要注意一个常见误解字段上有索引不代表所有包含该字段的查询都会走索引。比如你的查询条件是db.users.find({ name: { $regex: /^张/ } })即使你在 name 上建了索引因为使用了正则前缀匹配这个查询还是可以利用索引的但如果你用的是不区分大小写的正则或者在正则前面加了通配符索引基本就废了后面会详细展开。还有一个更隐蔽的情况有些字段虽然经常出现在查询条件里但它的基数Cardinality非常低。什么叫基数低就是字段可能的取值很少比如 status 只有 “active” 和 “inactive” 两种值gender 只有 “male” 和 “female”。对这种字段建索引索引本身的区分度很差优化器可能会认为走索引需要扫描太多索引条目反而不如集合扫描划算。所以单字段索引我更建议建在那些取值丰富、查询选择性高的字段上比如用户 ID、订单号、时间戳等。2.2 复合索引顺序和方向决定了性能上限实际业务里单字段索引经常不够用。最常见的场景是查询条件里同时有多个字段比如“查某个用户在某段时间内的订单”这时候你会想分别给 user_id 和 create_time 建两个单字段索引不就行了MongoDB 确实会尝试用索引合并Index Intersection来同时利用两个索引但效果通常不如一个设计良好的复合索引。复合索引的关键在于字段顺序。凡是涉及复合索引的查询优化器都是按照索引字段顺序来匹配的。最经典的一条经验法则叫“等值先行排序次之范围最后”。什么意思假设你要建一个索引来支持{ user_id: xxx, status: active, create_time: { $gt: 某个时间 } }这个查询那比较合理的索引顺序是{ user_id: 1, status: 1, create_time: 1 }。因为 user_id 是等值匹配status 也是等值匹配create_time 是范围匹配把等值字段放前面范围字段放最后索引扫描就能最大程度缩小范围。反过来如果你把 create_time 放在最前面那么即使你有 user_id 和 status 条件索引也没法先利用 user_id 来缩小范围只能先沿着时间轴扫一段再在结果里过滤 user_id扫描的索引条目会多出很多。这个排序规则我再强调一遍对于复合索引来说走不走索引、走得好不好跟你字段顺序的关系非常大。很多时候你建了复合索引却发现查询还是慢就是顺序没放对。除了顺序索引方向也很重要。升序索引用 1 表示和降序索引用 -1 表示对等值查询没有影响但对排序操作有直接影响。如果查询里既要按字段 A 排序又要按字段 B 排序而且两个方向不同比如sort({ price: -1, create_time: 1 })那么你的复合索引也应该建成方向一致的形式{ price: -1, create_time: 1 }。这样 MongoDB 就可以顺着索引直接返回结果而不需要在内存里做一次额外的排序。如果索引方向跟排序方向不一致MongoDB 可能还是会用索引扫描拿数据但随后必须多花一步 sort 阶段在数据量大时这一步非常吃内存和 CPU。2.3 覆盖查询和投影带来的额外红利索引影响性能还有一个很容易被忽略的点就是覆盖查询Covered Query。当一个查询需要的所有字段都包含在索引里时MongoDB 甚至不需要去读原始文档直接从索引中就能拿到全部结果。这种情况下查询的耗时几乎等于索引扫描耗时跟集合文档大小完全无关。比如你有这样一个索引db.orders.createIndex({ user_id: 1, status: 1, amount: 1 })然后执行查询db.orders.find( { user_id: u_10001, status: paid }, { _id: 0, user_id: 1, status: 1, amount: 1 } )这个查询的过滤条件正好命中索引的前两个字段投影里需要的字段也全在索引里而且排除了 _id。MongoDB 就会认为这是一个覆盖查询不需要再回表取文档。在数据量大、文档比较大的场景里覆盖查询对性能的提升极其明显你可以省掉大量的随机磁盘读取。要做到覆盖查询有两点需要注意。一是投影里不要带上索引之外的字段二是一定要显式排除 _id。默认情况下 MongoDB 的查询都会返回 _id而 _id 通常不在索引里所以一旦没有排除它就必须回表去拿 _id覆盖查询就失效了。实际开发中我发现很多人根本不知道这个细节明明索引建好了查询也走了索引但执行计划里还是会有 FETCH 阶段原因就在这里。2.4 索引类型补充TTL、唯一索引、文本索引什么时候用除了普通的单字段和复合索引MongoDB 还提供一些特殊索引类型它们也会明显影响查询性能或者系统行为。TTL 索引是一种特殊的单字段索引专门用于自动删除过期数据。它要求字段类型必须是日期类型MongoDB 会有一个后台线程定期扫描 TTL 索引把超过指定时间的文档删除。这类索引对查询本身也有加速作用尤其是你经常按时间过滤数据的场景反正都是索引建了不亏。需要特别注意的是TTL 索引只能是一个单字段索引不能跟其他字段组成复合索引。唯一索引则更多是业务约束层面的东西比如保证某个业务编号不能重复或者用户名不能重复。唯一索引在写入时会多做一次唯一性检查会带来一些额外的写入开销但对查询来说依然是很好的索引因为唯一索引的选择性是最高的优化器通常会非常偏好它。文本索引Text Index则是针对全文搜索场景设计的适合做简单的关键词搜索。但文本索引有自己的语法比如$text查询而且它不支持一些普通索引能覆盖的排序和范围查询场景。如果你只是想在标题里做模糊匹配很多时候普通的前缀正则查询加单字段索引就够用了不一定非要上文本索引文本索引的维护成本和查询限制都需要单独评估。3. 用 explain 实测量化索引的影响3.1 explain 的三种模式分别该看什么索引对查询性能的影响到底多大嘴上说了不算必须用 explain 看实际执行计划。MongoDB 的 explain 支持三种模式queryPlanner、executionStats、allPlansExecution。queryPlanner只生成查询计划不真正执行查询所以速度很快适合快速确认某个查询会不会走某个索引。它返回的winningPlan里会告诉你执行阶段是什么比如是IXSCAN索引扫描、COLLSCAN集合扫描还是FETCH从文档中取数据。日常开发里我通常先跑这一种确认查询有没有走预期的索引。executionStats会真的把查询执行一遍然后返回详细的执行统计信息包括扫描了多少条文档、多少条索引条目、执行耗时、是否发生了内存排序等。这个模式是分析索引性能的主力因为它的数据非常直观。要注意的是它默认会执行查询并返回结果在数据量很大的生产库上直接跑可能压力不小建议加db.collection.explain(executionStats).find(...)这种方式它依旧会执行但对线上影响相对可控。如果确实担心可以配合 limit 或使用queryPlanner先看计划。allPlansExecution会评估所有候选计划并针对每个计划都抓取执行统计。这个模式适合做更深层次的优化器行为分析但开销更大常规排查里用得不多。3.2 读懂关键指标totalDocsExamined 和 totalKeysExamined执行统计里对性能影响最直观的两个指标是totalDocsExamined扫描文档数和totalKeysExamined扫描索引条目数。这两个数字能非常清楚地告诉你查询效率到底怎么样。如果totalDocsExamined等于totalKeysExamined而且返回的结果数也差不多说明这个查询筛选效率很高每条索引条目几乎都能命中文档这是比较理想的状态。如果totalDocsExamined远远大于返回结果数说明你虽然走了索引但索引选择性和查询条件之间的匹配度不够高扫描了一大堆索引条目才筛出很少的结果。比如你在一个只有两种取值的字段上建了索引筛选出其中一种索引扫描可能扫了 50 万条索引条目最后只返回了 30 万条文档剩下的 20 万条并不是查询需要的而是后续过滤掉的。这种情况下索引不是没用而是用得不够高效。还有一种更糟的情况是totalDocsExamined等于整个集合的文档总量那基本就是 COLLSCAN索引完全没有参与要么没建索引要么查询条件不能让优化器使用索引。我在实际排查中养成了一个习惯看 explain 输出时先扫一眼winningPlan确认有没有IXSCAN然后再看两个 Examined 指标。如果存在SORT阶段还要额外看memUsage和是否触发了磁盘排序。一条性能良好的查询应该是扫描的索引条目数跟返回结果数在一个数量级而且没有额外的 SORT 或 COLLSCAN。3.3 一个典型对比有索引和无索引的差异实录为了让大家更直观地感受索引带来的差异我拿一个模拟场景来演示。假设有一个订单集合orders里面有 500 万条文档每条文档有一个user_id字段和一个order_amount字段。现在我要查某个用户的所有订单。没有索引的时候执行db.orders.explain(executionStats).find({ user_id: u_888888 })返回的统计大概是{ executionStats: { executionTimeMillis: 5820, totalDocsExamined: 5000000, totalKeysExamined: 0, winningPlan: { stage: COLLSCAN } } }500 万条文档全部扫了一遍耗时 5.8 秒。从用户体验的角度这个响应时间已经非常慢了如果请求量再大一点数据库 CPU 和 IO 很快就扛不住。然后建一个普通单字段索引db.orders.createIndex({ user_id: 1 })再次执行同样的查询explain 输出变成{ executionStats: { executionTimeMillis: 12, totalDocsExamined: 238, totalKeysExamined: 238, winningPlan: { stage: FETCH, inputStage: { stage: IXSCAN } } } }耗时从 5800 多毫秒降到了 12 毫秒扫描的文档数从 500 万降到 238。这是一个非常典型的对比索引能改变的不是快一点两点而是从秒级到毫秒级的跨越。当然这个例子里的耗时跟机器配置、数据分布都有关系但量级上的差异是实打实的。这也是我在面试或者带新人时最喜欢用的一个例子你不需要背任何调优口诀只要会用 explain对比一下 COLLSCAN 和 IXSCAN 的耗时、扫描量你自然就理解索引为什么重要了。4. 索引的代价和边界什么时候索引帮倒忙4.1 写入放大每个索引都是写入时的额外开销索引不是免费的午餐它最大的代价体现在写入性能上。每次插入一条文档MongoDB 不止要写入原始文档本身还要为这条文档在每个索引上都维护对应的索引条目。如果一个集合有 5 个索引那一次插入实际上的写入操作就是 1 次文档写入加上 5 次索引写入也就是 6 次左右的写入操作。对于更新操作来说也有类似的问题。如果你更新了一个被索引的字段MongoDB 需要删除旧索引条目再插入新索引条目这会带来额外的 B 树结构调整成本。如果你的业务是典型的写多读少比如日志采集、物联网传感器数据上报那么索引数量就必须严格控制否则写入吞吐量会急剧下降。我见过一个真实的场景某个团队往日志集合上加了四五个索引本意是方便后面按各种维度查询结果发现数据写入速度从每秒几万条掉到了每秒几千条最后只能下线一部分索引。所以建索引前一定要考虑读写比例读多写少的场景可以适当多建索引写多读少的场景要克制每个索引都要有明确的使用场景。4.2 内存占用和索引淘汰索引是常驻内存的或者说理想情况下尽量要常驻内存。MongoDB 使用内存映射文件管理数据索引页会被当作普通页一样缓存到内存里。如果索引太大无法完全放进内存就会出现频繁的页淘汰和磁盘加载性能会急剧恶化。实际经验中很多人只关注“集合大小”忘了看“索引大小”。通过db.collection.stats()可以查看totalIndexSize这个字段。如果你的服务器内存本来就紧张再叠加几个很大的索引那就有可能出现数据库明明没什么查询磁盘 IO 却很高的情况因为后台可能一直在换入换出索引页。有一个比较粗略的估算方式总索引大小建议控制在可用内存的一定比例以内具体比例没有绝对标准但如果你发现totalIndexSize已经超过内存的一定份额就要考虑是不是索引建得太多了或者部分大索引利用率很低该清理就清理。MongoDB 里可以用db.collection.getIndexes()查看索引列表再结合db.collection.aggregate()里的$indexStats查看每个索引的真实使用率。长期没有被使用的索引果断删掉一个长期闲置的索引对读性能没有任何帮助纯粹是负担。4.3 索引失效和低效的典型查询写法索引建好了也不代表所有查询都能享受它的加速。MongoDB 里有一类查询模式会让索引失效或者至少无法高效利用索引我列几个最常见的对索引字段使用$where表达式。$where的评估是在 JavaScript 引擎里做的无法利用索引只要查询里出现$whereMongoDB 基本上只能做集合扫描。对索引字段做不规范的$regex正则查询。正则表达式如果是以通配符开头比如$regex: /张$/索引也无法高效利用。但如果是前缀匹配比如/^张/在普通索引上是可以利用的。对索引字段使用了$not、$nin这类否定操作。否定操作的效率通常不高优化器很难通过索引缩小范围容易退化为大范围扫描甚至集合扫描。对索引字段进行了表达式转换。比如你在查询里写{ $where: this.price * 2 100 }或者隐式地对字段做了运算索引本身就失效了。需要说明的是MongoDB 不像关系型数据库那样有函数索引的通用能力所以这种场景很难通过调整 SQL 来解决最好是新增一个预先计算好的字段并对它建索引。还有一种很容易被忽略的低效场景复合索引本来设计得不错但在查询时你省略了前置字段。比如你有索引{ user_id: 1, status: 1, create_time: -1 }但查询条件只用了{ status: active }没有包含user_id那这个复合索引就只能起到一个非常有限的作用MongoDB 虽然也可能走索引但扫描范围会非常大效果甚至不如一个专门针对 status 建的单字段索引。查询条件必须满足复合索引的前缀原则才能真正发挥索引价值。4.4 没有银弹选择性、基数、数据分布的影响最后再聊一个比较抽象但非常重要的点索引的效果其实取决于数据分布。选择性好的索引性能提升是数量级的选择性差的索引效果可能微乎其微。什么叫选择性简单说就是某个字段的不同值数量占总文档数比例。用户 ID 一共 500 万基本每个值都不同这就是高选择性索引能快速定位到极少数文档。但性别字段只有两个值这就是低选择性索引本质上是把一个 500 万的集合分成两个 250 万的子集查询结果仍然很大优化器会觉得还不如全表扫描一次来得直接。另外还有数据分布倾斜的问题。比如 status 字段有 10 个值但其中 99% 的文档都是 “active”如果你查询的是那个只有 1% 的 “closed”索引效果依然很好但如果你查询的是 “active”索引扫描可能需要扫出 99% 的文档代价极高。这种情况下优化器有可能会放弃索引转而选择集合扫描因为集合扫描的线性读在某些场景下比大量随机读索引页更快。明白了这一点你就能理解为什么“只要建了索引就会快”这种说法是错的更合理的做法是结合业务查询模式选择那些真正能缩小数据范围的字段建索引。5. 一个完整优化案例从慢查询到稳定快速的排查过程5.1 慢查询现象我接手过一个模拟订单系统的性能问题。当时线上反馈说某个统计报表的接口越来越慢最严重的时候一次请求要 20 多秒已经影响到体验。这个接口的底层查询大概是这样的db.order_records.find({ region: 华东, create_time: { $gte: ISODate(2024-01-01), $lt: ISODate(2024-02-01) }, channel: app }).sort({ create_time: -1 }).limit(50)这个查询本身不复杂就三个过滤条件加一个排序加一个 limit。问题在于 order_records 集合已经膨胀到了接近 2000 万条文档而查询里涉及的字段之前完全没有任何索引。5.2 第一步用 explain 定位根因我先执行了 explainexecutionStats 模式确认当前执行计划db.order_records.explain(executionStats).find({ region: 华东, create_time: { $gte: ISODate(2024-01-01), $lt: ISODate(2024-02-01) }, channel: app }).sort({ create_time: -1 }).limit(50)结果显示 winningPlan 里的阶段是 COLLSCANtotalDocsExamined 差不多等于全部 2000 万条文档执行耗时十几秒。这就是典型的全表扫描。这时候我做的第一件事没有直接建索引而是先跟业务确认了一下这个统计报表的调用频率和相关字段的查询价值。因为这个集合每天还在大量写入索引不能乱加否则后面写入性能可能会出问题。5.3 第二步设计复合索引并验证根据查询条件region 和 channel 都是等值匹配create_time 是范围匹配而且需要按 create_time 倒序返回。结合前面说的“等值先行范围放最后排序要跟索引方向一致”我设计了这样一个复合索引db.order_records.createIndex({ region: 1, channel: 1, create_time: -1 })create_time 用 -1是为了跟查询里的sort({ create_time: -1 })保持一致。等值字段 region 和 channel 放前面范围字段 create_time 放最后。建完索引之后我重新跑了一遍 explaindb.order_records.explain(executionStats).find({ region: 华东, create_time: { $gte: ISODate(2024-01-01), $lt: ISODate(2024-02-01) }, channel: app }).sort({ create_time: -1 }).limit(50)这次执行计划变成了 FETCH IXSCAN索引扫描的条目数和返回结果数都在合理范围内执行时间降到了 200 毫秒以内。同一个查询从十几秒到 200 毫秒索引的威力就是这么直接。5.4 第三步关注排序开销和覆盖查询优化索引建好之后explain 结果里已经看不到额外的 SORT 阶段了因为索引方向跟排序方向一致MongoDB 直接按索引顺序返回数据省掉了内存排序。不过我还想再压一压这个查询的耗时。因为这个报表只关心部分字段我发现投影里其实只需要 region、channel、create_time、order_amount 这几个字段。那就让索引包含字段order_amount更新索引为db.order_records.createIndex({ region: 1, channel: 1, create_time: -1, order_amount: 1 })这样查询如果做投影只保留这四个字段并排除 _id就可以变成覆盖查询连文档读取都能省掉。当然覆盖查询对字段有严格要求不能有 _id这一点前面已经说过。在这个案例的实际业务里我们通过覆盖查询把接口的响应时间进一步压缩到了几十毫秒级别效果非常理想。这个案例其实没有什么高深的技术整个思路就是先确认瓶颈是全表扫描再按照查询模式设计复合索引最后用 explain 验证每一步的优化效果。这套流程几乎可以复用到任何 MongoDB 慢查询排查里。6. 常见问题与避坑技巧速查6.1 索引排查里遇到的高频问题我在不同项目里反复遇到过一些差不多的问题整理成一张速查表方便你对照排查症状可能原因排查方向查询还是很慢但明明加了索引查询条件不满足复合索引前缀原则用 explain 看 winningPlan 是不是 IXSCAN以及 totalKeysExamined 指标索引建了执行计划偶尔走索引偶尔全表扫描数据分布倾斜优化器重新评估了代价查看集合 stats确认字段基数必要时用 hint 强制索引查询计划一直没切换成新索引查询计划缓存未失效查询计划缓存清理或临时用 hint 验证效果写入变慢明显索引过多写入放大严重用 $indexStats 检查每个索引的使用率删除长期未使用的索引一个简单查询返回数据很少扫描文档数却巨大查询条件里的字段有隐式类型转换或使用了非高效操作符检查查询条件写法避免正则、$where、否定操作等更新一个字段导致大量延迟被更新的字段在索引里需要维护索引条目考虑索引字段是否有必要包含该字段这个表里的每一行都是我实际踩过或者帮别人排查过的价值不在于表格本身而在于你遇到类似问题时能有一个清晰的起点。6.2 几个只有实操才能发现的细节最后分享几个我在实际操作中积累的小经验这些常规文档里很少专门提到但遇到过一次你就忘不掉。第一复合索引字段顺序不是拍脑袋定的。你在设计时要严格按“等值字段前置、排序字段次之、范围字段最后”的规则来。如果你不确定索引顺序对不对最快的方法就是用 explain 对比当前索引和候选索引下的totalKeysExamined哪个扫描的索引条目少哪个通常就更合适。第二用hint强制索引前后测试时要留意优化器缓存。我自己习惯的做法是在测试环境先把新旧索引都建好然后用 hint 分别执行同一条查询对比耗时和扫描量确认哪个方案更好再决定留存哪个索引。这个流程看似繁琐其实是在避免“凭感觉”做优化。第三不要忽略查询计划缓存。线上改了索引之后如果查询时间没有变化不一定是索引无效可以先清理这个集合的查询计划缓存然后再观察。具体命令因版本略有不同但思路是一样的你先确认新索引确实更优再让优化器重新评估。第四监视索引使用率很重要。我一般会间隔一段时间跑一次$indexStats那些长期没有access次数的索引基本就是死索引留着只会拖累写入和占用内存。删之前再确认一下有没有被哪个冷门查询用到没问题就清理掉让数据库保持轻装上阵。第五小心排序与索引方向的配合。很多人建索引时根本不关注 1 和 -1只关心建了没有。实际上一个反向的排序可能会导致 MongoDB 额外做一次内存排序数据量大时内存根本排不下只能落盘那查询从毫秒级变成秒级就是一瞬间的事。7. 用好索引本质上是理解数据访问模式说到最后我想把话题稍微拉高一点。索引对查询性能的影响本质上不是“加不加索引”这个二元问题而是“你理不理解自己的数据访问模式”的问题。一个集合到底该建几个索引、每个索引包含哪些字段、字段顺序怎么排全部取决于你的查询是怎么写的、排序是怎么排的、哪些字段是高频过滤条件。我见过很多系统索引建了一堆但没有一个是真正贴合业务查询的。也见过一些系统索引数量极少但因为每个索引都建到了点子上整体查询性能非常稳定。这两者的差别不在于谁会的命令多而在于谁在动手建索引之前愿意多花几分钟时间分析一下查询模式再用 explain 验证而不是上来就对着字段闭眼建索引。如果你正在处理一个新的 MongoDB 项目我建议你从第一个查询写出来的时候就开始考虑索引。不要等到数据量大了、接口变慢了再回头补那样不仅排查成本高而且数据迁移和索引重建的压力也不小。索引这件事越早规划收益越大踩的坑也越少。