复合索引必须遵循esr原则,范围查询与sort同时存在时字段顺序错误会导致内存排序;排序字段需为索引连续前缀且方向严格匹配;explain("executionstats")是验证索引是否生效的唯一可靠依据。

复合索引字段顺序必须遵循 ESR 原则
范围查询(如 {$gt}、{$in}、{$lt})和 sort() 同时出现时,索引字段顺序错了,排序就用不上索引,直接触发内存排序(SORT 阶段),nscannedObjects 会远高于 nscanned。MongoDB 的 B-tree 索引是嵌套排序的:一旦某个字段是范围条件,它之后所有字段在索引中不再全局有序。
- ✅ 正确顺序:
{status: 1, created_at: -1, amount: 1}→ 先等值匹配status,再用created_at支撑sort({created_at: -1}),最后在已限定时间范围内做amount: {$gt: 100} - ❌ 错误顺序:
{status: 1, amount: 1, created_at: -1}→amount是范围,created_at失去有序性,sort只能内存执行,可能因超 100 MB 内存限制而报错或写磁盘 - ⚠️
sort方向必须严格匹配索引方向:查询.sort({a: 1, b: -1})只能命中{a: 1, b: -1}或{a: -1, b: 1},但不能命中{a: 1, b: 1}
explain("executionStats") 是唯一可信判断依据
别靠“有索引”就以为优化到位。真正要看的是执行计划里是否出现 SORT 阶段,以及 totalDocsExamined 是否显著大于 totalKeysExamined。这两个数字差距大,基本就是范围字段位置不当的铁证。
- 运行带
explain("executionStats")的查询,重点看executionStages中有没有{"stage": "SORT"} - 检查
executionStats.nReturnedvsexecutionStats.totalDocsExamined:如果后者是前者的几倍甚至几十倍,说明 IXSCAN 返回了大量无关文档,后续全靠 FETCH + SORT 补救 - 注意
executionStats.executionTimeMillis和executionStats.totalKeysExamined的比值——若 keys examined 很高但返回很少,大概率是范围字段前置过度或顺序错位
高选择性范围字段可以破例前置,但必须验证
ESR 是默认安全线,不是教条。当某个范围字段本身区分度极高(比如 user_id: {$gt: "U999999999"} 能筛掉 95% 文档),而等值字段低选择性(如 status: "active" 匹配 80% 文档),这时把高选择性范围字段提前,可能减少 IXSCAN 扫描量。
- 先建候选索引:
{user_id: 1, status: 1, created_at: -1} - 用
explain("executionStats")对比原索引和新索引的totalKeysExamined和executionTimeMillis - 注意:这种破例只对特定查询有效,不能泛化;写多读少场景下还要权衡索引维护成本
多字段排序必须严格匹配索引前缀
复合索引支持排序的前提是:排序字段构成索引的**连续前缀**,且方向一致或完全相反。哪怕只差一个字段顺序或方向,MongoDB 就放弃索引排序。
- 索引
{a: 1, b: -1, c: 1}可以支撑.sort({a: 1, b: -1})或.sort({a: -1, b: 1}),但不支持.sort({a: 1, c: 1})(跳过b)、也不支持.sort({b: -1, a: 1})(顺序颠倒) - 如果查询含
.sort({a: 1, b: -1, c: 1}),索引必须是{a: 1, b: -1, c: 1}或{a: -1, b: 1, c: -1};混搭方向(如{a: 1, b: 1, c: -1})无法命中 - 实际中常见陷阱:开发写了
.sort({updated_at: -1, _id: 1}),却只建了{_id: 1, updated_at: -1}—— 因为_id不是前缀,排序完全失效
explain("executionStats") 看真实扫描量,而不是凭经验猜“应该快”。最容易被忽略的,是排序方向与索引方向的严格一致性——差一个 1 和 -1,整个排序就退回到内存阶段。











