totalDocsExamined远大于totalKeysExamined说明索引未被有效利用或未走索引,主因包括分片键未显式建索引、使用$ne/$regex等索引拒绝型操作符、mongos广播扫描、覆盖索引字段缺失或顺序不符、稀疏索引导致路由失效等。

直接看 executionStats.totalDocsExamined 和 executionStats.totalKeysExamined:如果前者远大于后者(比如 10 倍以上),说明索引没被有效利用,或者压根没走索引。
为什么 totalDocsExamined 高得离谱?
常见错误现象是:明明建了索引,explain("executionStats") 却显示扫描了几万甚至几十万文档,而索引条目只查了几百个。
- 分片键没显式建索引 —— MongoDB 不会自动为分片键创建索引,
db.collection.getIndexes()里找不到{ shardKeyField: 1 }就等于裸奔 - 查询用了
$ne、$not、$regex(非前缀匹配)、$exists: false等操作符,MongoDB 很可能放弃索引走全量扫描 - 在分片集群中,
mongos无法把查询条件精确下推到目标分片,导致广播式扫描(所有分片都执行一遍) - 投影字段或查询字段缺一个,就掉出覆盖索引范围 —— 比如索引是
{ region: 1, status: 1 },但查询里要amount字段且没包含在索引中,MongoDB 必须回表读文档
怎么验证是不是覆盖索引生效了?
覆盖索引不是“有索引就行”,它有三个硬性条件,缺一不可。
- 查询条件中所有字段(含隐式字段,如分片键)必须全部落在同一个索引里,且顺序符合 ESR(等值 → 排序 → 范围)
-
.find({}, { fieldA: 1, fieldB: 1, _id: 0 })中列出的所有字段,也必须全部出现在该索引中 - 不能含
$text、$where、$expr、$lookup(无pipeline下推时)等强制加载文档的操作
例如分片键是 region,常用查询是 db.orders.find({ region: "us-east", status: "shipped" }, { orderNo: 1, amount: 1, _id: 0 }),那必须建索引:db.orders.createIndex({ region: 1, status: 1, orderNo: 1, amount: 1 })。少一个字段,totalDocsExamined 就会飙升。
分片集合上建索引的几个关键陷阱
在分片环境里,createIndex 容易踩坑,而且报错信息不直观。
- 别在单个 shard 上执行
createIndex—— 必须在mongos上运行,否则索引只存在于某一分片,查询时不可见 -
background: true在 MongoDB 6.0+ 分片集合上无效,还会让mongos报错;必须用background: false(前台构建),否则写入会被阻塞 - 如果索引字段包含数组,而分片键又不是该数组字段,MongoDB 可能拒绝创建,报错
cannot index parallel array - 绝对不要给分片键建稀疏索引(
sparse: true)—— 分片路由逻辑依赖分片键严格存在,稀疏索引会导致部分文档被路由丢失
小文档多时扫描数虚高,其实是 IO 问题
当集合里全是几字段的小文档(如传感器每秒一条 { timestamp: ..., value: ... }),totalDocsExamined 看似不高,但实际性能差,因为 WiredTiger 为每条文档单独分配页、生成 key、维护细碎索引条目,导致严重存储碎片化。一次逻辑查询可能触发数十次随机磁盘读 —— 这时候 totalDocsExamined 没骗人,但瓶颈不在扫描逻辑,而在底层 IO 效率。
- 改用文档桶模式:按时间/业务聚合小文档,例如每小时一个桶,
data存数组,加bucket_start/bucket_end字段建索引 - 配合聚合管道下推:用
$filter在桶内筛选,$arrayElemAt提取目标项,避免展开整个数组 - 注意桶大小平衡:太小(如每分钟一桶)仍碎片化;太大(如每天一桶)导致单文档过大、更新锁粒度粗、WiredTiger 缓存压力高
真正卡住的往往不是“扫多少文档”,而是“扫这些文档花了多少次磁盘寻道”。优化时先分清:是逻辑扫描失控,还是物理 IO 碎片化 —— 二者解法完全不同。


















