多键索引因每个数组元素生成独立索引条目且不合并压缩,导致internalPages占比高、tree.depth≥6、缓存占用远超磁盘大小;应优先删除未使用的僵尸索引,改用点号路径索引、预展开字段或聚合管道优化,并避免多数组字段复合索引。

多键索引(multikey index)本身就会显著放大内存占用,尤其在数组字段元素多、文档量大时,WiredTiger 缓存中常驻的索引页可能远超磁盘大小——这不是配置没调好,而是结构特性决定的。
为什么 multikey 索引比普通索引更吃内存
WiredTiger 对 multikey 索引的 B+ 树节点不做“合并压缩”,每个数组元素都会生成独立索引条目,并映射到不同内部页。比如一个文档 {"tags": ["a", "b", "c", "d"]} 在 {tags: 1} 索引下会生成 4 条索引记录,且它们大概率分散在不同页上。这直接导致:
- internalPages 占比飙升(
db.collection.stats({indexDetails: true})中indexDetails.<name>.tree.internalPages</name>> 40% 就危险) - tree.depth 容易 ≥ 6,预热整棵树时 cache 压力陡增
- 同样 100MB 磁盘索引,cache 里可能常驻 300MB+ 的页(填充率低 + 页分裂严重)
删掉冗余 multikey 索引前先确认它是否真被用到
别急着 drop,先查真实访问情况。很多 multikey 索引是“僵尸索引”——建了但从不命中:
- 用
db.collection.stats({indexDetails: true})检查accesses.ops,长期为 0 就是纯占坑 - 跑典型查询加
.explain("executionStats"),看executionStages.stage是IXSCAN还是COLLSCAN;再核对keysExamined/nReturned比值,500:1 说明区分度极差,索引基本无效 - 注意:即使查询用了
$in或$elemMatch,也不代表 multikey 索引高效——低基数字段(如status只有 3 个值)单独建 multikey 索引,99% 是浪费
替代 multikey 索引的三种更省内存方案
不是所有数组查询都必须靠 multikey 索引硬扛:
- 对固定结构嵌套数组(如
{"scores": [{"subject": "math", "score": 95}]}),改用点号路径索引:db.collection.createIndex({"scores.subject": 1, "scores.score": -1})—— 它不是 multikey,B+ 树结构干净,depth 和 internalPages 都可控 - 高频等值查询(如
tags: "redis")且 tags 元素少(≤ 5),可考虑预展开字段:{"tag1": "redis", "tag2": "mongodb", ...}+ 单字段索引,避免 multikey 开销 - 用聚合管道提前过滤:对
$unwind后的数据做$match,配合allowDiskUse: true把压力从内存转移到磁盘临时文件,比让 multikey 索引把 cache 塞爆更可控
如果必须保留 multikey 索引,至少控制它的生长边界
放任不管会让问题随数据增长指数恶化:
- 禁止在复合索引里混用多个数组字段——
db.collection.createIndex({"tags": 1, "categories": 1})会直接失败,这是 WiredTiger 的硬限制,别尝试绕过 - 避免用
{createdAt: -1}这类降序字段和 multikey 组合,页分裂更剧烈;优先选升序 +_id这类天然单调字段做后缀 - 批量写入前临时删索引:
db.collection.dropIndexes()→ 写完 →db.collection.createIndex(..., {background: true}),实测快 3–5 倍,且不会把 cache 推爆
multikey 索引的内存代价是结构性的,不是靠调大 cacheSizeGB 就能解决的。真正要盯紧的,是 indexDetails.<name>.tree.depth</name> 和 internalPages 这两个数字——它们比总索引大小更能暴露问题本质。


















