MongoDB写操作会同步更新所有相关索引,导致写放大:1次文档写入需同步执行N次索引更新,引发I/O、内存与日志压力;应通过indexStats和explain识别并删除未被查询使用的冗余索引。

每个写操作都要同步更新所有相关索引
MongoDB 在执行 insert、update 或 delete 时,不是只改数据文件,而是会遍历该集合上**所有命中条件的索引**,逐个插入、修改或删除对应索引条目。这意味着:一个 insert 操作写入 1 条文档,若该集合有 5 个索引,MongoDB 实际要执行 6 次写(1 次数据 + 5 次索引),且这些写是串行同步完成的。
常见错误现象:db.collection.insertOne({...}) 延迟突然升高,mongostat 显示 netIn 不高但 qr(queued reads)和 qw(queued writes)持续上涨;慢日志里出现大量 writeConflicts 或 numYields 增多。
- 稀疏索引(
sparse: true)和部分索引(partialFilterExpression)只在满足条件时才参与更新,但判断是否“命中”本身也消耗 CPU - 复合索引中未被写操作涉及的字段,只要索引整体被触发(比如 update 修改了其中任一字段),整个索引条目仍需重写
- 索引键值越长(如对长字符串、嵌套对象全量索引)、重复度越低(如唯一索引),索引 B+ 树分裂越频繁,写放大越明显
索引越多,WiredTiger 缓存压力越大
WiredTiger 引擎将索引页和数据页一起缓存在内存中。索引占用空间虽小,但其访问局部性差——写操作触发的索引更新往往分散在不同页,导致更多缓存页被标记为 dirty 并刷盘。当索引总数超过可用 RAM 的合理比例(例如 >30%),就会挤占数据页缓存,间接加剧磁盘 I/O 竞争。
使用场景:副本集主节点写入吞吐骤降,但 serverStatus.mem.resident 接近上限,wiredTiger.cache.tracked_dirty_bytes 持续高于 1GB;同时 extra_info.page_preempted 显著上升,说明缓存页被强制踢出。
- 用
db.collection.stats()查看indexCount和indexSize,再对比storageStats.size,若索引占比 >20%,值得审查 -
explain("executionStats")中executionTimeMillis高但docsExamined低,往往是索引维护开销掩盖了真实查询耗时 - 避免对低基数字段(如
state: {0, 1})单独建索引——选择率太低,几乎不加速查询,却白吃写性能
日志写入与索引更新形成双重 I/O 放大
MongoDB 默认开启 journal 日志,所有写操作先写日志再刷数据/索引页。索引越多 → 单次写涉及的逻辑修改越多 → 日志记录体积越大 → 更容易触发 commitIntervalMs 提前刷盘。尤其当日志和数据文件共用同一块磁盘时,I/O 队列深度激增。
参数差异:j:true 的写关注会让日志刷盘更频繁;而默认的 commitIntervalMs=100 毫秒,在高并发写场景下可能造成日志写入堆积,进一步拖慢索引更新节奏。
- 监控
journal.earlyCommits(提前提交次数)和journal.commits比值,若 >0.3,说明日志压力已影响写入调度 - 不要盲目调大
commitIntervalMs来缓解——这会提高崩溃丢失数据的风险,应优先删冗余索引 - 生产环境建议将 journal 目录挂载到独立 SSD 设备,与 data 目录物理隔离
如何快速识别“过度索引”
真正影响写的不是索引数量本身,而是**有多少索引实际被查询用到**。一个从未出现在 explain().queryPlanner.winningPlan 中的索引,纯属写入负担。
实操建议:
- 用
db.collection.aggregate([{$indexStats: {}}, {$group: {_id: "$key", accesses: {$sum: "$accesses.ops"}}}])查看各索引最近访问频次(需开启 indexStats) - 检查慢日志中的
planSummary字段,统计哪些索引高频出现;长期没出现在任何IXSCAN中的索引,可标记待删除 - 对复合索引,用
db.collection.getIndexes()看key顺序,确认是否符合查询谓词前缀匹配(例如索引{a:1,b:1,c:1}只能支撑{a:1}、{a:1,b:1}、{a:1,b:1,c:1}查询,{b:1}或{c:1}无效) - 删除前务必用
db.collection.dropIndex("name")而非dropIndexes(),避免误删 _id 索引
索引不是越多越好,而是越精准越好。删掉一个不用的复合索引,有时比加十个新索引更能稳住写入 P99 延迟。


















