MongoDB写操作索引有效性须通过explain("executionStats")验证update/delete的查询部分:删索引后若stage变为COLLSCAN且totalDocsExamined暴增,说明该索引对写定位仍关键;同时监控wiredTiger.cache.dirty、oplog.window、writeLock.waitTimeMs三指标变化。

用 explain("executionStats") 确认索引是否真被写操作绕过
很多人误以为删了索引就自动提升写入性能,但实际得先验证写操作是否真的不走那个索引。MongoDB 的 explain("executionStats") 对写操作无效,但它对 find() + updateOne() 组合的查询部分有效——也就是说,如果你的 updateOne({ status: "pending" }, { $set: { processed: true } }) 依赖某个索引定位文档,那删掉该索引后,explain 会显示 "stage": "COLLSCAN",且 totalDocsExamined 暴涨。这说明写前过滤变慢了,反而可能拖累整体吞吐。
实操建议:
- 对所有高频
update/delete查询语句,先跑一遍explain("executionStats"),记下totalKeysExamined和totalDocsExamined - 删除目标索引后重跑,若
totalDocsExamined显著上升(比如从 100 → 50000),说明该索引对写定位仍有关键作用,不能直接删 - 特别注意带
$or、$in或多字段条件的更新,它们更容易因缺失索引而退化为全表扫描
在副本集 secondary 节点上做写压测,避免主节点抖动
直接在 primary 上删索引+压测,容易引发复制延迟、oplog 队列堆积,甚至触发故障转移。正确做法是把测试流量导向一个临时降级的 secondary 节点:停掉它的 priority: 0 和 votes: 0,再手动执行 rs.stepDown() 让它彻底脱离选举,然后在上面删索引、跑压测。
这样做的好处是:
- 写入压力不会影响主节点的响应时间或复制链路
- 你可以安全地用
mongostat观察netIn、netOut、flushes和page faults,对比删索引前后的变化 - 如果压测出问题,直接
rs.add()把它加回副本集即可,无数据丢失风险
监控三个硬指标:wiredTiger.cache.dirty, oplog.window, 和 writeLock.waitTimeMs
索引变更对写吞吐的影响,最终会反映在存储引擎和复制层的底层指标上,而不是简单的“QPS 上升了 X%”。重点关注:
-
wiredTiger.cache.dirty:单位 MB,表示缓存中未刷盘的脏页。删冗余索引后,这个值应明显下降(写入时要维护的索引结构变少) -
oplog.window(通过rs.printSecondaryReplicationInfo()查):单位秒,代表当前 oplog 覆盖的时间窗口。若删索引后该值缩短,说明写入积压加剧,oplog 正在被快速填满 -
writeLock.waitTimeMs(来自db.serverStatus().locks.Global.acquireCount.w对应的 wait time):毫秒级,反映写锁排队时间。OLTP 场景下超过 5ms 就值得警惕
这些指标必须在相同负载下连续采集至少 5 分钟,才能排除瞬时抖动干扰。
别忽略 _id 索引以外的“隐性索引”:TTL 和 text 索引的写开销更大
很多人只盯着普通字段索引,却忘了 expireAfterSeconds TTL 索引和 text 索引对写入的额外负担。TTL 索引每秒都会触发后台扫描,而 text 索引在插入时要分词、去停用词、建倒排链——实测显示,一个含 3 个中文字段的 text 索引,会让单条插入耗时增加 12–18ms(WiredTiger 引擎,SSD 环境)。
检查方式很简单:
- 运行
db.collection.getIndexes(),留意"expireAfterSeconds"或"text"类型的索引 - 临时禁用 TTL:修改
expireAfterSeconds: 0(不是删,避免重建);对text索引,可先dropIndex()再用小批量数据压测 - 这类索引一旦存在,即使查询不用,写入时也必然参与维护——没有“惰性更新”一说
真正影响线上写吞吐的,往往不是那几个显眼的复合索引,而是这些默认开着、没人定期 review 的“背景服务型索引”。











