executionStats.totalKeysExamined 过高,核心原因通常是索引扫描范围过大。常见诱因包括:索引字段顺序不符合 ESR 原则、缺少 $elemMatch 导致多键索引无法进行边界交集裁剪、覆盖索引字段不完整,以及分片查询没有携带等值分片键等问题。

为什么executionStats.totalKeysExamined会远高于预期
当索引扫描键数量明显偏高时,本质上就是 MongoDB 在索引树中扫描了过多无效范围。这并不一定说明索引完全失效,更常见的是:索引命中了,但查询条件无法有效收紧扫描边界,或者字段顺序破坏了 ESR(等值 → 排序 → 范围)这一索引设计原则。比如查询条件是 { status: "active", createdAt: { $gt: ISODate("2025-01-01") } },如果索引定义成 { createdAt: 1, status: 1 },那么 MongoDB 往往只能先扫描所有 createdAt > 2025-01-01 的索引键,再逐条过滤 status,这时 totalKeysExamined 就很容易远超预期。
- 使用
.explain("executionStats")查看executionStats.executionStages.indexBounds,确认实际索引扫描范围是否远比预估更宽 - 检查查询语句中是否包含
$ne、$not、$regex(非前缀匹配)、$exists(false)等条件——这些操作通常会削弱索引范围扫描能力,甚至退化为索引全扫描或 COLLSCAN - 复合索引设计时,等值匹配字段应放在最左侧,并且顺序尽量与查询模式一致;例如
{ a: 1, b: 1, c: 1 }这个索引并不能高效支持{ b: 1, c: 1 }这类查询
如何让多键索引真正“缩小边界”
数组字段建立多键索引之后,$elemMatch 是让 MongoDB 对多个数组条件执行边界交集的关键操作符。缺少它时,即便写了类似 { tags: { $gte: "a", $lt: "z" } } 的条件,MongoDB 往往也只能扫描较大的 tags 索引区间,难以真正做到精确裁剪。
- 多个数组内条件应使用
$elemMatch包裹,例如{ grades: { $elemMatch: { $gte: 90, $lte: 99 } } },这样才能触发边界交集,把扫描范围从[ -inf, +inf ]明显收窄到[ 90, 99 ] - 不要拆成彼此独立的条件:像
{ "grades.0": { $gte: 90 }, "grades.1": { $lte: 99 } }这种写法既无法形成交集,也不会走多键索引的优化路径 - 如果数组元素本身还是嵌套文档,要确保
$elemMatch内涉及的字段同样包含在索引中,且字段顺序匹配,否则边界交集优化仍然可能失效
覆盖索引不是“建了就快”,而是“字段一个都不能少”
覆盖索引的真正目标,是让 MongoDB 只访问索引就完成查询,而不再回表读取文档。但只要查询字段或投影字段缺失任意一个,执行计划就可能回退到文档读取,这时 totalKeysExamined 和 totalDocsExamined 往往都会上升——因为不仅索引扫描没有减少,文档还被额外读取了一次。
- 索引中应完整包含:所有查询条件字段 + 所有投影字段(当设置
_id: 0时,可不必包含_id) - 字段顺序仍需遵循 ESR 原则:例如查询为
{ region: "us-east", status: "shipped", updatedAt: { $gt: ... } },投影为{ orderNo: 1, amount: 1 },那么更合理的索引写法是{ region: 1, status: 1, updatedAt: 1, orderNo: 1, amount: 1 } - 不要在覆盖索引中随意加入未参与查询的冗余字段——这不仅无法提升查询性能,反而会扩大索引体积、增加写入成本,并带来更多内存占用
分片集合上建索引,totalKeysExamined 高往往是因为路由失败
在 MongoDB 分片集群中,如果 mongos 无法把查询精准路由到目标分片,就会向所有分片广播请求。这样每个分片都会分别扫描本地索引,最终的 totalKeysExamined 就会变成所有分片扫描键数的总和。很多时候,这并不是索引本身设计错误,而是查询条件没有有效命中分片键。
- 先确认查询条件中是否带有分片键的等值匹配(例如
{ shardKey: "value" });缺少这类条件时,广播查询通常很难避免 - 检查分片键字段是否位于索引中且靠左;即使存在
{ status: 1, shardKey: 1 }这样的索引,MongoDB 也无法据此高效路由,通常应保证类似{ shardKey: 1, ... }的前缀结构 - 通过
.explain("executionStats")展开executionStats.shards,逐个分析各分片的totalKeysExamined分布是否均衡;如果某些分片为 0,而其他分片特别高,往往说明查询路由或分片命中逻辑存在问题
如果想真正降低 MongoDB 的索引扫描键数量,重点并不是盲目增加索引,而是让查询条件与索引结构精确匹配。实际排查中,很多性能问题都出在这些容易被忽略的细节上:字段顺序错了一位、覆盖索引少了一个字段、遗漏了 $elemMatch,或者分片查询时没有带上分片键。看上去执行计划里只有一句 “IXSCAN”,似乎已经使用了索引,但真正导致 totalKeysExamined 偏高的原因,往往就藏在这些细微之处。
