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)等操作符——它们会让 MongoDB 放弃使用索引范围扫描,退化为索引全扫甚至 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 完全不读文档,只查索引。但只要投影字段或查询字段漏掉一个,就会 fallback 到文档加载,同时 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 高往往是因为路由失败
在分片集群中,mongos 如果没法把查询精确下推到目标分片,就会广播给所有分片执行——每个分片都扫一遍自己的局部索引,totalKeysExamined 就是所有分片扫描数之和。这不是索引建得不好,而是查询条件没锚定分片键。
- 确认查询中是否带分片键等值条件(如
{ shardKey: "value" });没有它,几乎必然广播 - 检查分片键字段是否在索引中且位置靠左;即使建了
{ status: 1, shardKey: 1 },MongoDB 也无法用它路由,必须是{ shardKey: 1, ... } - 用
.explain("executionStats")展开executionStats.shards,逐个看每个分片的totalKeysExamined是否均匀;如果某个分片为 0、其他全高,说明路由逻辑出问题
降低扫描键数的关键,从来不是堆更多索引,而是让每一个查询条件都能被索引结构“精准咬住”。最容易被忽略的是:字段顺序错一位、少投一个字段、漏写 $elemMatch、或者在分片查询里忘了带上分片键——这些都会让 totalKeysExamined 陡增数倍,而日志里可能只显示 “IXSCAN”,看不出问题在哪。











