高频读写混合场景下,索引设计须兼顾写入吞吐与查询延迟:写入超1500 ops/s且多字段更新时禁建非覆盖复合索引;点查建唯一索引,范围分页按esr建复合索引,聚合字段不建索引;须通过explain验证ixscan覆盖、无fetch/sort阶段;并定期清理低访问索引以缓解写放大。

高频读写混合场景下,索引设计必须同时扛住每秒数千次的写入压力和毫秒级响应的查询需求,稍有偏差就会导致写放大激增或查询退化为全表扫描。
明确读写特征与索引权衡边界
先用 db.collection.stats() 查看集合当前写入量级:重点关注 count、size 和 avgObjSize,再结合业务监控确认真实 QPS 分布。若写操作 >1500 ops/s 且平均文档更新涉及 ≥3 个字段,则禁止在该集合上新建非覆盖型复合索引——WiredTiger 缓存会因索引同步频繁刷脏页,实测吞吐下降 40% 以上。
读请求必须分类:对 【高频点查(如用户 ID 查询)】 单独建唯一索引;对 【带分页的范围查询(如订单时间区间+状态)】 严格按 ESR 顺序建复合索引;对 【仅用于聚合 pipeline 的字段】 禁止建索引,改用 $lookup + 内存排序更稳。
执行计划验证必须覆盖三类典型查询
方法一:点查类(等值+投影)
运行 db.orders.find({userId: "U123"}, {orderId: 1, status: 1, _id: 0}).explain("executionStats"),检查 winningPlan.stage 是否为 IXSCAN,且 totalDocsExamined === nReturned。若出现 FETCH 阶段,说明索引未覆盖投影字段,必须补全字段或删掉未索引字段。
方法二:分页列表类(等值+范围+排序)
执行 db.orders.find({status: "paid", createdAt: {$gte: ISODate("2026-09-01")}}).sort({amount: -1}).limit(20).explain("executionStats"),确认 stage 是 IXSCAN 而非 SORT;若 executionStages.stage === "SORT",证明排序字段未放在索引末尾,立即调整索引字段顺序。
方法三:写后即查类(写入后 5 秒内查最新记录)
在应用层模拟写入后立刻执行 db.orders.find({userId: "U123"}).sort({_id: -1}).limit(1),观察 executionStats.executionTimeMillis 是否稳定 ≤8ms。若波动超过 ±15ms,说明 _id 索引被写入竞争阻塞,需启用 writeConcern: {w: "majority", j: true} 并关闭 readConcern "local"。
索引生命周期管理
第一步:每周跑一次索引访问统计
db.orders.aggregate([{$indexStats: {}}, {$group: {_id: "$name", accesses: {$sum: "$accesses"}, lastAccess: {$max: "$lastAccess"}}}])
第二步:标记待清理索引
筛选出 accesses 且 <code>lastAccess 的索引名,例如 <code>idx_userId_createdAt_status —— 若该索引对应查询已下线或改走 Redis 缓存,则进入删除队列。
第三步:安全删除
在低峰期执行 db.orders.dropIndex("idx_userId_createdAt_status"),注意:删除前必须确认该索引不在任何 explain() 的 winningPlan 中,否则会触发隐式 COLLSCAN。
第四步:写入压测验证
用 mongostat --host rs0/localhost:27017 --rowcount 10 监控删除后 10 分钟内的 netIn、netOut 和 flushes,若 flushes 下降超 30%,说明写放大缓解成功。











