复合索引能一次性支持等值查询、范围查询和排序,而多个单字段索引无法协同工作;其字段顺序必须为“等值+范围+排序”,且排序方向需与索引一致,否则索引失效或引发全表扫描。

为什么范围分页必须用复合索引,而不是多个单字段索引
因为MongoDB的查询优化器无法把多个单字段索引“拼起来”用于同一个查询。比如你有 {createdAt: -1} 和 {status: 1} 两个独立索引,但执行 find({status: "active"}).sort({createdAt: -1}) 时,它只能选其中一个——通常选排序字段的索引,导致 status 条件走全表扫描。
复合索引则把条件和排序固化在同一个B树结构里,让查询能一次性命中。关键不是“有没有索引”,而是“索引字段顺序是否匹配查询模式”。
- 等值查询字段(如
status: "active")必须放在复合索引最左端 - 范围查询字段(如
createdAt: { $lt: ... })紧随其后,且只能有一个 - 排序字段如果与范围字段相同(如按
createdAt分页),就不用额外加;如果不同(如查status后再按score排序),score必须是索引最后一个字段,且方向要和sort()一致
复合索引字段顺序怎么排:等值 + 范围 + 排序
以电商订单分页为例:查 userId: 123 下所有订单,按 createdAt 倒序,下一页基于上一页最后一条的 createdAt 值做 $lt 查询。
正确索引是:db.orders.createIndex({ userId: 1, createdAt: -1 })
错误示例包括:
-
{ createdAt: -1, userId: 1 }:违反最左前缀,userId等值条件无法使用索引 -
{ userId: 1, status: 1, createdAt: -1 }:如果查询没带status,这个三字段索引就只用到userId,createdAt部分失效 -
{ userId: 1, createdAt: 1 }:索引升序,但查询sort({createdAt: -1})方向相反,旧版本MongoDB会退化为内存排序
如何验证索引是否真正生效
别只看 getIndexes() 返回了什么,要用 explain("executionStats") 看实际执行路径。
重点关注三项:
-
stage字段是否为"IXSCAN"(而非"COLLSCAN"或"SORT") -
nReturned是否等于你预期返回的条数(比如limit(10)就该是10) -
executionTimeMillis是否稳定在毫秒级;如果 >100ms,大概率没走对索引
特别注意:nScannedObjects 应该接近 nReturned。如果前者远大于后者(比如返回10条却扫了5万行),说明索引覆盖不全,可能缺字段或顺序错。
游标分页中重复值导致漏数据怎么办
当排序字段(如 createdAt)存在大量重复值时,仅靠 { createdAt: { $lt: lastDoc.createdAt } } 会跳过同时间戳的其他文档。
解决方法是把唯一字段(如 _id)追加进复合索引和查询条件:
- 建索引:
db.logs.createIndex({ createdAt: -1, _id: -1 }) - 分页查询:
find({ createdAt: { $lt: lastDoc.createdAt }, _id: { $lt: lastDoc._id } }).sort({ createdAt: -1, _id: -1 })
这样即使时间戳一样,也能靠 _id 进一步区分顺序。注意:两个字段排序方向必须一致,否则索引无法被完全利用。
这个细节在日志、事件类集合中极易被忽略,一旦数据高频写入,漏数据问题会在第3–5页之后集中暴露。


















