MongoDB复合索引本质是按声明顺序和方向构建的B+树,排序方向必须与sort()完全一致才能线性遍历,否则触发内存排序;等值查询字段须置前,范围查询后字段丧失排序能力。

索引底层是B+树,方向不匹配就无法线性遍历
MongoDB复合索引本质是按你声明的字段顺序和方向构建的一棵B+树。比如 { a: 1, b: -1, c: 1 },它的物理存储顺序是:先按 a 升序排,a 相同时再按 b 降序排,b 也相同时再按 c 升序排。这个顺序是固定的,不可逆向“局部翻转”。当你执行 .sort({ a: 1, b: 1, c: 1 }),引擎想从索引里顺序读出结果,但发现 b 的实际排列是倒的——它没法一边往前走 a,一边又让 b 正着走,只能中断索引扫描,把数据捞出来再内存排序。
方向错一个,explain里立刻出现SORT节点
哪怕只把一个字段的方向写反,比如索引是 { status: 1, createdAt: -1 },但查询写成 .sort({ status: 1, createdAt: 1 }),MongoDB 就不会用这个索引做排序。验证方式非常直接:
db.orders.find({ status: "done" }).sort({ status: 1, createdAt: 1 }).explain("executionStats")
看输出里的 executionStages.stage:如果是 IXSCAN 后紧跟 SORT,说明排序没走索引;如果只有 IXSCAN 且 docsExamined === nReturned,才真正用了索引排序。
- 单字段索引(如
{ ts: 1 })可支持.sort({ ts: -1 }),这是特例,引擎能反向遍历整棵树 - 多字段时没有这种“全局反向”捷径,必须每个字段方向都对,或全部反过来(
{ a: -1, b: 1 }可支持.sort({ a: 1, b: -1 })) -
createIndex()返回ok: 1不代表它被用了,只是建成功了
等值查询字段必须放最前,否则排序字段直接失效
如果你的查询带 .find({ userId: "U123", status: { $in: ["A", "B"] } }) 再 .sort({ createdAt: -1 }),那 status 是范围查询($in 在多数场景下等价于范围),它后面的 createdAt 就丧失排序能力——即使索引是 { userId: 1, status: 1, createdAt: -1 },createdAt 字段也无法用于排序,因为 status 的多个取值会打断 createdAt 的连续有序性。
- 等值字段(
userId: "U123")放最前,能快速定位到一个子树 - 最多一个范围字段(
$gt,$in等)跟在后面,它之后的所有字段都不能参与排序 - 想让
createdAt参与排序?要么把status改成等值,要么把createdAt提到status前面(但得确保查询条件也包含createdAt等值)
中文、null、BSON类型会让排序行为更隐蔽
即使索引和 sort() 方向完全一致,也可能因数据类型踩坑:
- 字段含
null值时,MongoDB 把null当作最小值处理,升序排最前,降序排最后——这和某些应用预期不符 - 中文字符串默认按 UTF-8 字节序排,不是拼音序;要按拼音排序必须显式加
collation: { locale: "zh" },且索引也得用同样collation创建,否则不匹配 - 同一字段混用数字和字符串(如
"10"和10),BSON 类型不同会导致排序错乱,且索引无法统一优化
这些细节不会报错,但结果不稳定,查半天才发现是数据类型埋的雷。











