MongoDB复合索引必须严格遵循ESR(Equal→Sort→Range)物理顺序,否则索引可能部分失效或完全不生效;因B-tree索引依赖谓词在树中的可跳过性,Range字段会终止后续字段的定位与排序能力,$in属Equality类别,多等值字段顺序可互换但须全在Sort前。

直接按 Equality、Sort、Range 三类操作的顺序排字段,不是“先写等值再写范围”,而是必须严格满足 E → S → R 的物理层级结构——否则索引可能被部分跳过或完全不生效。
为什么 (a, b, c) 不一定比 (a, c, b) 好
MongoDB 的 B-tree 索引不是靠“左前缀匹配”驱动的,而是靠查询谓词在索引树中的可跳过性。当查询是 db.col.find({a: 1, b: {$gt: 2}}).sort({c: 1}) 时:
-
a: 1是Equality,必须放最前 -
c: 1是Sort,要让排序免去内存in-memory sort,它得紧接在等值字段之后,且索引中该字段的排序方向(1或-1)必须与查询一致 -
b: {$gt: 2}是Range,只能放在最后;如果它插在中间(比如(a, b, c)),MongoDB 就无法利用c做排序——因为范围扫描会打断有序性
所以正确索引是 db.col.createIndex({a: 1, c: 1, b: 1}),而不是照搬 MySQL 思维的 (a, b, c)。
多个字段都用 $in 时,仍算作 Equality
$in 在 MongoDB 中不触发 Range 行为,只要它出现在查询条件里,就归入 Equality 类别。例如:
db.col.find({a: {$in: [1,2,3]}, c: 5}).sort({b: -1})
这里 a 和 c 都是等值(a 是多值等值),b 是排序字段,没有 Range 字段。索引应为 {a: 1, c: 1, b: -1} 或 {c: 1, a: 1, b: -1}——两个等值字段顺序可互换,但都必须在 b 前。
注意:$in 值过多(比如上千个)会导致索引扫描膨胀,此时不如拆成多次查询或改用其他策略。
Range 字段后不能再跟任何字段(包括 Sort)
一旦索引中出现 Range 字段(如 {$gt}、{$regex}、{$ne}),它右边的所有字段都无法被用于索引查找或排序。例如:
- 索引
{a: 1, b: 1, c: 1}无法支持.sort({c: 1})如果查询含b: {$gt: 5} - 但索引
{a: 1, c: 1, b: 1}可以:因为c在b前,排序发生在范围之前 - 索引
{a: 1, b: 1, c: 1}可以支持find({a: 1, c: 5})(利用前缀{a: 1}),但效率低于{a: 1, c: 1},因为要遍历所有a: 1下的b值才能定位c
换句话说,Range 是索引使用的“终止符”——它之后的字段只参与过滤(IXSCAN 阶段),不参与定位或排序。
索引交集能绕过 ESR,但代价高
MongoDB 允许用多个单字段/复合索引做交集(index intersection),比如为上面的查询建 {a: 1, c: 1} + {b: 1}。这确实能工作,但:
- 交集仅在 4.2+ 版本默认启用,旧版本需显式设置
allowDiskUse或依赖优化器判断 - 交集需要额外的内存和 CPU 去合并结果集,延迟更高
- 交集不支持排序——即使你有
{a: 1, c: 1},.sort({c: 1})仍可能触发in-memory sort - 运维更复杂:要同时维护多个索引的生命周期、大小、更新开销
所以,ESR 不是“建议”,而是设计主索引时的刚性约束;交集只是兜底手段,不是替代方案。
真正容易被忽略的是:排序字段的方向(1 或 -1)必须和 .sort() 完全一致,哪怕查询只含一个字段——sort({c: -1}) 无法使用 {c: 1} 索引做排序加速,哪怕它能用于查找。


















