复合索引字段顺序必须按「查询条件→排序→范围过滤」优先级排列,等值匹配字段(如status、user_id)须放最前作锚点,高区分度字段优先;排序字段紧接等值字段后且其后不可有范围条件;范围字段只能置末尾,否则索引失效。

复合索引字段顺序不是按查询频率排,而是严格按「查询条件 → 排序 → 范围过滤」的优先级来定。顺序错一个,索引就可能完全失效。
哪些字段必须放最前面?
必须是查询中用 $eq 精确匹配的字段,且出现在所有高频查询的 WHERE 条件里。这类字段能直接把扫描范围缩到最小,是索引的“锚点”。
- 如果查询总是带
{ status: "active" },那status就该放第一位 - 如果有些查询不带
status,但都带user_id,那就得把user_id放第一位 - 多个
$eq字段时,优先选区分度高(cardinality 高)的,比如order_id比is_paid更适合作为首位
排序字段能不能放中间?
可以,但仅限于它紧接在所有 $eq 字段之后,且后面不能再跟范围操作符($gt、$lt、$in 等)。一旦排序字段后面出现范围条件,MongoDB 就无法利用该排序做内存优化,会退化成内存排序(sort stage)。
- 支持:
{ user_id: 1, created_at: -1 }+ 查询{ user_id: 123 }+.sort({ created_at: -1 }) - 不支持:
{ user_id: 1, created_at: -1, amount: 1 }+ 查询{ user_id: 123, amount: { $gt: 100 } }+.sort({ created_at: -1 })—— 这时created_at的排序能力被amount的范围打断,索引无法避免内存排序
范围字段为什么只能放最后?
因为 B-tree 索引是按字段顺序逐层排序的。一旦某个字段用了范围查询,后续字段的索引值就不再连续有序,MongoDB 没法继续用索引做定位或排序。
- 错误顺序:
{ status: 1, created_at: 1, amount: 1 }+ 查询{ status: "pending", created_at: { $gte: ISODate("2025-01-01") } }——amount完全没机会被索引覆盖 - 正确顺序:
{ status: 1, created_at: 1 }就够了;如果还要查amount,就得另建一个以amount开头的索引,或者把amount放最后并确保它只用于等值匹配 - 注意:
$in算等值集合,不算范围,所以{ a: 1, b: 1 }支持{ a: 1, b: { $in: [2,3,4] } },但不支持{ a: 1, b: { $gt: 2 } }
explain() 里怎么验证字段顺序是否合理?
关键看 executionStats.nReturned 和 executionStats.totalKeysExamined 的比值,再结合 executionStats.executionStages.stage 是否出现 SORT 或 COLLSCAN。
- 如果
totalKeysExamined远大于nReturned(比如 1000 vs 10),说明索引扫描太宽,可能是等值字段没放前缀位 - 如果出现
"stage" : "SORT"且"memUsage" : xxx很大,基本确认排序字段被范围条件阻断了 - 如果
"stage" : "IXSCAN"但"nReturned"很小、"keysExamined"却极大,大概率是范围字段放太前,导致后置字段失效
最容易被忽略的是:同一个字段在不同查询中角色可能不同——有时是等值条件,有时是范围条件。这种情况下,没有万能顺序,得按主查询路径设计索引,再用 hint() 或额外索引兜底。











