索引越多写入越慢是真实存在的,因每次增删改都要同步更新所有相关索引;实测qps超500且索引数>8时延迟明显上升,尤其在ssd或内存受限环境更显著。

索引越多,写入越慢是真实存在的
每新增一个索引,MongoDB 在 insert、update、delete 时都要同步更新所有相关索引。实测显示:当集合写入 QPS 超过 500,且索引数 > 8 个时,写入延迟会明显抬升,尤其在 SSD 有限或内存不足的部署中更敏感。这不是理论警告,而是 mongostat 中能直接看到 idxCount 与 qr/ qw(读/写队列)的正相关趋势。
- 单文档写入耗时 = 原始写入 + 每个匹配索引的 B 树插入开销
- 复合索引比多个单字段索引更省空间、更少写放大(例如
{a:1,b:1}可覆盖{a:1}查询,无需额外建{a:1}) - 被
explain("executionStats")显示为indexOnly: false的索引,大概率是冗余的——它没被查询真正用上
只保留被实际查询命中的索引
别靠“可能有用”留索引。用 db.setProfilingLevel(1, { slowms: 100 }) 开启慢查询日志后,结合 db.system.profile.find().sort({ ts: -1 }).limit(10) 查最近慢操作,再对每个慢查询跑 explain("executionStats")。重点看 executionStats.executionStages.stage 是否为 IXSCAN,以及 nReturned 与 totalDocsExamined 的比值 —— 如果后者远大于前者,说明索引没过滤好,或根本没走索引。
- 删除未被使用的索引:
db.collection.dropIndex("name_of_index"),不要依赖“以后可能用上” - 注意
$or查询:它可能触发多个索引,但每个分支仍需独立命中索引;如果某分支总走全表扫描,那个索引就该删 - Agenda 等框架自带的默认索引(如
findAndLockNextJobIndex)要验证是否真被调度逻辑调用,否则删掉
用覆盖索引减少文档读取
覆盖索引(Covered Query)指查询和投影字段全部包含在同一个索引中,MongoDB 不需要回查原始文档。这能显著降低磁盘 IO 和内存压力,尤其在高并发读场景下。但它要求你明确知道哪些字段常一起出现于 find() 的 filter 和 projection 中。
- 示例:若常执行
db.orders.find({ status: "shipped", createdAt: { $gt: ISODate("...") } }, { orderId: 1, customerId: 1, _id: 0 }),则建{ status: 1, createdAt: 1, orderId: 1, customerId: 1 }复合索引 - 注意字段顺序:等值查询字段(
status)放前,范围查询字段(createdAt)放后,投影字段跟在最后 - 避免在覆盖索引里包含大字段(如长文本、数组),否则索引体积膨胀,反而拖慢 B 树遍历
复合索引的字段顺序不能乱
字段顺序决定索引能否被复用。MongoDB 只能高效使用索引的“前缀” —— 即从左到右连续匹配的字段。比如索引 { a: 1, b: 1, c: 1 } 支持 { a: 1 }、{ a: 1, b: 1 }、{ a: 1, b: 1, c: 1 } 查询,但不支持 { b: 1 } 或 { a: 1, c: 1 }(除非配合 $hint 强制,但这是反模式)。
- 高频等值查询字段优先(如
tenantId、userId) - 然后是范围查询字段(
createdAt、updatedAt) - 最后才是用于排序或投影的字段(
score: -1、name: 1) - 降序(
-1)和升序(1)混合时,若涉及$sort,必须严格匹配索引方向,否则无法利用索引排序











