索引树越深、内部页越多,内存压力越大——WiredTiger缓存会加载大量非叶子页,导致resident内存激增;查indexDetails.tree.depth和internalPages占比,结合serverStatus.cache使用率可精准定位“胖索引”。

索引树越大,内存越扛不住
MongoDB 的 B-Tree 索引不是“存在磁盘上就没事了”——WiredTiger 引擎会把整个索引树的内部节点(尤其是非叶子页)尽可能加载进内存缓存池(wiredTiger.cache)。树深度每增加一层,意味着每次查询至少多一次内存随机访问;而页分裂越多,索引结构越稀疏、碎片越多,实际驻留内存的页数反而更多。这不是理论推演,是 2026 年多个线上事故的共性根源:某订单库加了 2dsphere + 复合时间戳索引后,B-Tree 深度从 3 跳到 7,db.serverStatus().mem.resident 直接涨了 8.2GB。
怎么查你当前索引树到底有多“胖”
别只看 db.collection.stats() 里 totalSize,那只是磁盘占用。真正压内存的是索引页在缓存中的“热身状态”。关键要看:
-
db.collection.stats({indexDetails: true})中每个索引的indexDetails.<name>.tree字段(需 MongoDB ≥ 6.0),它会返回depth、entries、internalPages—— 尤其关注internalPages占比是否 > 15% -
db.runCommand({serverStatus: 1}).wiredTiger.cache里的bytes currently in the cache和maximum bytes configured,如果前者长期 > 90% 阈值,说明索引树+数据页正在挤占全部缓存空间 - 用
mongostat --host <host>观察net列持续高getmores,往往意味着索引页没全进内存,被迫频繁换页
页分裂不是“自动优化”,而是内存泄漏前兆
B-Tree 页分裂本身不可怕,可怕的是分裂后旧页不回收、新页不断膨胀。WiredTiger 默认不会主动合并稀疏索引页,尤其当写入模式高度倾斜(比如按时间递增插入 + 高频更新某个字段)时,_id 索引可能没事,但 {status: 1, createdAt: -1} 这类复合索引极易出现“半空页堆积”。后果很直接:db.collection.stats().indexCount 没变,但 indexDetails.<name>.tree.internalPages 每天涨 3–5%,三个月后内存多吃 4GB。
- 紧急缓解:对问题索引执行
db.collection.reIndex()(注意锁表影响) - 长期规避:避免在高基数字段(如
email)和低基数字段(如status)上建升序复合索引;改用{status: 1, _id: 1}替代{status: 1, createdAt: -1},利用_id的天然均匀分布压制分裂 - 验证方法:reIndex 后立刻跑
db.collection.stats({indexDetails: true}),对比tree.depth是否下降、tree.internalPages是否回落 20%+
别信“索引越多查得越快”,B-Tree 深度超标后反而更慢
当索引树深度 ≥ 6,且命中率(indexDetails.<name>.accesses.ops / 总查询数)低于 0.3 时,这个索引大概率在拖后腿。MongoDB 查询优化器不是总选最优索引——它可能因为统计信息滞后,误选一个深树索引去扫 200 万条,而跳过另一个浅但字段匹配度稍低的索引。
- 查真实命中率:
db.collection.aggregate([{$indexStats: {}}, {$group: {_id: "$name", ops: {$sum: "$ops"}}}]) - 删冗余索引前先禁用:
db.collection.hideIndex("name_1_status_1")(≥ 5.0),观察 48 小时 QPS 和 P99 延迟是否恶化 - 地理空间索引特别危险:
2dsphere索引的 B-Tree 实际是 GeoHash 分层树,10GB 数据集下深度常达 8–10,务必搭配maxDistance或box严格约束范围,否则一查就爆内存
真正卡住性能的,往往不是没索引,而是某个索引太“胖”、太“深”、又太久没人管。树的大小不是静态数字,是写入节奏、字段分布、版本行为共同喂出来的活物。










