复合索引字段顺序必须严格遵循最左匹配原则:查询条件须从索引最左字段开始连续匹配,跳过中间字段则后续字段无法利用索引;排序字段顺序与方向必须与索引完全一致或全反向;高选择性字段应前置以提升剪枝效率。

复合索引字段顺序必须匹配查询条件的最左前缀
MongoDB 不会“智能匹配”字段顺序,只认最左连续字段。比如索引 {userId: 1, status: 1, createdAt: -1},以下查询能走索引:
-
{userId: "U123"}→ 用到userId前缀 -
{userId: "U123", status: "PENDING"}→ 用到前两个字段 -
{userId: "U123", status: "PENDING", createdAt: {"$gt": ISODate("2026-01-01")}}→ 全部命中
但这些写法完全不走该索引:
-
{status: "PENDING"}→ 缺少userId,跳过最左字段,直接全表扫(COLLSCAN) -
{userId: "U123", createdAt: {"$gt": ...}}→ 跳过status,createdAt条件无法下推,只用到userId,后续过滤靠内存扫描
验证方式:用 explain("executionStats") 看 executionStages.stage 是否为 IXSCAN,且里面没有 SORT 或 COLLSCAN 节点。
排序字段必须与索引定义顺序和方向严格一致
如果查询带 .sort({status: 1, userId: 1}),而索引是 {userId: 1, status: 1},MongoDB 无法复用该索引做排序——因为索引里是先按 userId 排、再按 status 排,和你要的顺序冲突。
能避免内存排序的唯一情况是:
-
.sort()字段顺序与索引定义完全相同(如{userId: 1, status: 1}→.sort({userId: 1, status: 1})) - 或全部反向(如索引
{userId: 1, status: -1}可支持.sort({userId: -1, status: 1}))
注意:.sort({status: 1, userId: 1}) 永远不能用 {userId: 1, status: 1} 加速,哪怕字段一模一样。
高选择性字段优先放前面
选择性 = distinct value count / total documents。把低选择性字段(比如只有 "ACTIVE"/"INACTIVE" 的 status)放在前面,相当于先把数据粗分成两桶,每桶仍含大量文档,B+ 树剪枝效果差,IO 和内存开销反而上升。
更优做法:
- 把
userId(通常唯一或接近唯一)放最左 - 其次放中等选择性字段,如
tenantId或region - 最后放低选择性字段,如
status、isDeleted
范围查询字段(如 {"$gt": ...})也建议靠后——MongoDB 对复合索引中第一个范围条件之后的所有字段,无法利用索引排序或精确匹配。
多数组字段会阻止复合多键索引创建
如果想建 {tags: 1, categories: 1} 这类索引,但 tags 和 categories 都是数组字段,MongoDB 直接拒绝创建,报错 Cannot create a multikey index on multiple fields。
可行方案:
- 只保留一个数组字段在复合索引中,比如
{tags: 1, createdAt: 1}(createdAt是标量) - 或者改用通配符索引(
{"$**": 1}),但代价是索引体积大、写入慢 - 业务上拆分:把
categories改成字符串或枚举,避免数组
已有复合多键索引时,插入同时含多个数组字段的文档也会失败,不是警告,是硬错误。
实际建索引时,最容易被忽略的是「排序需求」和「范围条件位置」的耦合——很多人只盯着 where 条件,忘了 .sort() 和 $gt/$lt 在索引里的物理顺序是绑定的。一旦排序字段没对齐,就算过滤全走索引,SORT 阶段仍可能吃光内存甚至触发超时。











