复合索引字段顺序必须按 $eq → $range → $sort 排列,否则后续字段失效;覆盖索引需投影仅含索引字段且排除 _id;写多读少时应精简索引并避免高频更新字段置于索引左侧。

复合索引字段顺序决定查询是否能命中
字段顺序不是按业务重要性排,而是严格按查询条件中 $eq → $range(如 $gt、$in)→ $sort 的顺序来定。MongoDB 只能高效利用索引前缀,一旦中间出现范围查询,后续字段就无法用于过滤或排序。
- 错误示例:
db.orders.createIndex({status: 1, createdAt: -1, userId: 1}),若查询是{status: "paid", userId: "u123"},userId实际不生效——因为createdAt被跳过,索引前缀断裂 - 正确写法:高频等值查询字段放最左,比如
userId和status总是一起出现,且都是$eq,那应优先把选择性更高的字段放前面(如userId唯一性强于status),再跟createdAt用于排序 - 验证方式:用
db.orders.find({userId: "u123", status: "paid"}).explain("executionStats"),重点看executionStages.stage是否为IXSCAN,且nReturned≈totalDocsExamined
覆盖索引能直接避免文档回表
当查询投影(projection)只包含索引字段时,MongoDB 可完全在内存中完成查询,不读取原始文档。这对高频读场景(如订单列表页)提升显著。
- 适用场景:查列表页只显示
orderId、status、amount、updatedAt,而这些字段恰好都在索引里 - 建法示例:
db.orders.createIndex({userId: 1, status: 1, updatedAt: -1}, {projection: {orderId: 1, status: 1, amount: 1, updatedAt: 1, _id: 0}})—— 注意_id: 0显式排除,否则默认包含,破坏覆盖性 - 陷阱:如果查询用了
$text或$regex,即使字段在索引中,也无法触发覆盖;另外,_id默认总被返回,除非显式设为0
写多读少场景要警惕索引写放大
每个索引都会增加插入/更新/删除的开销。10 个索引可能让单条 insertOne 的延迟翻倍,尤其在高并发写入时。
- 判断依据:用
db.orders.stats().indexCount查当前索引数,结合db.serverStatus().metrics.document中的updated和deleted每秒速率评估压力 - 精简策略:合并功能重叠的索引,例如已有
{userId: 1, status: 1},就不必再单独建{userId: 1};用db.orders.getIndexes()检查是否有key: {xxx: 1}是其他复合索引的前缀 - 临时禁用:对低频后台任务(如导出报表)可临时用
hint()强制走某个索引,避免为它单独建索引
更新操作中索引字段变动会触发文档移动
如果 updateOne 修改了索引字段(尤其是复合索引中的前导字段),且新值导致文档在 B-tree 中位置变化,WiredTiger 可能需要重写整个文档块,产生额外 IO。
- 典型风险操作:
db.users.updateOne({_id: "u1"}, {$set: {status: "inactive", lastLogin: new Date()}})—— 若status在索引最左,且集合使用默认存储引擎(WiredTiger),文档很可能被迁移 - 缓解方法:对高频更新字段(如状态、计数器),尽量不放在复合索引左侧;或启用
allowDiskUse: true配合聚合更新,减少单次更新压力 - 监控指标:关注
db.serverStatus().metrics.record中的moved计数,持续增长说明索引设计与更新模式冲突
createIndex 命令,可能在写入高峰时悄悄拖慢整个集群。











