范围字段不能放在排序字段后,因b-tree索引按字段顺序嵌套排序,范围查询使后续字段失去全局有序性,导致排序无法利用索引而触发内存排序,显著增加nscannedobjects和sort阶段开销。

范围查询在 MongoDB 中容易拖慢性能,关键不在“有没有索引”,而在复合索引里范围字段是否排在 sort 字段之后、equality 字段之前——违反这个顺序,索引就只能用上一部分,甚至退化为索引扫描后全量过滤。
为什么范围字段不能放在排序字段后面?
MongoDB 的 B-tree 索引是按字段顺序逐层嵌套排序的。一旦遇到范围查询(如 {$gt: 10}、{$in: [...]}),后续字段的索引顺序就失效了:索引无法保证这些字段在范围区间内仍保持全局有序。
比如查询 db.orders.find({status: "paid", amount: {$gt: 100}}).sort({created_at: -1}),如果建索引为 {status: 1, created_at: -1, amount: 1},MongoDB 只能利用前两个字段做等值+排序,amount 的范围条件必须在内存中过滤,nscannedObjects 会远大于 nscanned。
- 索引字段顺序错位 →
IXSCAN阶段返回大量无关文档 → 后续FETCH和SORT开销陡增 -
explain("executionStats")中若出现"totalDocsExamined" > "totalKeysExamined",基本就是范围字段位置不当的信号
ESR 原则不是教条,但得先满足它再谈例外
ESR(Equality → Sort → Range)是设计复合索引时最稳妥的起点,尤其适用于读多写少、查询模式稳定的场景。它不是强制规则,但跳过它需要明确理由和验证。
以查询 db.logs.find({service: "api", level: {$in: ["error", "warn"]}}).sort({timestamp: -1}) 为例:
- ✅ 推荐索引:
{service: 1, timestamp: -1, level: 1}—— 先等值匹配service,再用timestamp支撑排序,最后让level在已限定的时间范围内做范围筛选 - ❌ 危险索引:
{service: 1, level: 1, timestamp: -1}——level是范围,它后面的timestamp无法用于排序,MongoDB 会 fallback 到内存排序(in-memory sort),可能触发SortStage错误或超内存限制
注意:sort 字段方向(1 或 -1)必须与查询中一致;若查询同时含升序和降序(如 .sort({a: 1, b: -1})),索引也必须严格匹配方向,否则无法命中。
当范围字段高选择性时,可以前置,但要验
极少数情况下,某个范围字段本身区分度极高(比如 user_id 上的 {$gt: "U123456789"} 能直接筛掉 95% 文档),而等值字段低选择性(如 status: "active" 匹配 80% 文档),这时把高选择性范围字段提前,可能减少 IXSCAN 扫描量。
但必须通过 explain() 对比验证:
- 检查
"executionStages.nReturned"是否显著下降 - 确认
"executionStages.works"没因排序缺失而暴涨 - 观察是否有
"stage": "SORT"出现在执行计划中 —— 有,说明排序没走索引,风险已引入
别依赖直觉。线上集合数据分布变化后,原本“合理”的顺序可能一夜变差。
别忽略索引覆盖和冗余
如果查询只投影部分字段(如 .find({a:1}, {b:1, c:1, _id:0})),且你建的复合索引包含全部这些字段(如 {a:1, b:1, c:1}),MongoDB 就能直接从索引返回结果,跳过 FETCH 阶段——这对高频范围查询的吞吐提升明显。
同时注意冗余:
- 已有
{a:1, b:1, c:1},就不用再单独建{a:1}或{a:1, b:1} - 但若存在独立查询
db.coll.find({b:1}),那{a:1, b:1, c:1}无法支持,仍需保留{b:1}或调整顺序
真正容易被忽略的,是范围字段类型隐式转换带来的索引失效——比如字段存的是字符串 "100",查询却用数字 {$gt: 100},即使有索引,MongoDB 也无法安全比较,会跳过索引走全表扫描。











