索引存在但未热加载是导致“nReturned低、totalDocsExamined高”的典型原因,即索引在磁盘上但未载入内存缓存,查询需频繁磁盘I/O;可通过explain()、serverStatus()缓存命中率及预热查询验证并优化。

索引在磁盘上但没进内存,explain() 显示 "executionStats.executionStages.nReturned" 很低但 "totalDocsExamined" 很高
这是典型“索引存在但没被热加载”的表现:MongoDB 的 WiredTiger 引擎会把索引页按需载入内存(cache),如果索引太大、访问不频繁,或 cacheSizeGB 设置过小,索引页就可能被换出。此时查询仍走索引,但每次都要从磁盘读取索引块,I/O 拖慢整个流程。
验证方式很简单:db.collection.explain("executionStats").find({field: value}) 查看 executionStats 中的 totalKeysExamined 和 totalDocsExamined 是否接近(说明索引生效),再对比 executionTimeMillis 和 executionStats.executionStages.works —— 若后者远大于前者,大概率是磁盘 I/O 占主导。
- 用
db.serverStatus().metrics.wiredTiger.cache检查当前 cache 命中率:"bytes currently in the cache"与"maximum bytes configured"的比值低于 80% 就值得警惕 - 确认
storage.wiredTiger.engineConfig.cacheSizeGB是否被人为压得太低(比如设成 1GB 但物理内存有 64GB) - 不要依赖“索引已创建”就认为它“随时可用”——冷索引首次查询必然慢,尤其复合索引或大字段索引
createIndex() 后立即查还是慢?可能是索引构建未完成或未预热
MongoDB 创建索引默认是后台异步(background: true),但即使建完,索引页也不会自动刷进内存。你看到 db.collection.getIndexes() 里有它,不代表它已热身。
- 新建索引后,手动触发一次覆盖式查询来“预热”:
db.collection.find({indexedField: {$exists: true}}).limit(1).hint({indexedField: 1}),强制加载索引根节点和部分叶节点 - 避免在高峰期建大索引;若必须建,加
{background: false}让它阻塞但确保索引结构一次性写稳(注意:这会锁表) - 对关键集合,可在部署后执行
db.collection.find().hint({yourIndex: 1}).itcount()(小数据集)或分批skip/limit扫描,让索引页逐步进入 cache
WiredTiger cache 不够用,cacheSizeGB 怎么调才不翻车
很多人直接把 cacheSizeGB 设为物理内存的 50%,但忽略了:WiredTiger cache 只管索引+文档页,而 MongoDB 进程本身、OS 文件缓存、日志缓冲区也要吃内存。设太高反而引发 swap 或 OOM。
- 先查真实压力:
db.serverStatus().mem看"resident"(实际驻留内存)和"virtual"(虚拟内存)差值;若resident长期逼近cacheSizeGB,说明 cache 确实不够 - 保守调整法:每次 +2GB,观察
serverStatus.wiredTiger.cache."bytes currently in the cache"是否稳定在新上限的 70–90%,同时监控 OS 层free -h的 available 内存是否仍 >2GB - 别忽略
storage.wiredTiger.collectionConfig.blockCompressor:启用snappy压缩能提升有效 cache 容量,但会略增 CPU;zlib压缩率更高,但更耗 CPU
索引碎片多、页分裂严重,compact() 能不能救急
长期高频写入+删除会导致索引页碎片化,即使索引在内存里,查找路径也变长。这时 compact() 可重建索引结构、合并页、释放空闲空间,间接提升内存访问效率。
-
db.runCommand({compact: "collectionName"})是唯一能在线整理索引物理布局的操作,但它会短暂阻塞写入(读不受影响) - 只对写入密集、且
db.collection.stats().indexDetails中某个索引的"size"明显大于"count" * avgObjSize / 3的集合考虑使用 - 注意:
compact()不释放磁盘空间给 OS(除非配合storage.mmapv1.smallFiles: true,但该引擎已弃用),它只是内部重组;真正释放磁盘得靠db.repairDatabase()(停机操作,慎用)
最常被忽略的一点:索引能否进内存,不只取决于大小,还取决于访问局部性。一个 500MB 的索引,如果每天只被查 3 次,它永远不如一个 50MB 但每秒被查上百次的索引“热”。与其盲目扩 cache,不如先用 system.profile 确认哪些索引真正在被高频使用。


















