必须用复合索引,因单字段createdAt索引无法同时高效支持等值过滤、排序与分页;正确顺序为等值字段在前、排序字段在后,如{status:1,createdAt:-1}。

为什么用 createdAt 做游标分页必须配复合索引?
单靠 createdAt 字段建索引,在真实业务中大概率不够用。因为绝大多数查询都带等值过滤条件(比如 { status: "active" } 或 { userId: "u123" }),如果只对 createdAt 单独建索引,MongoDB 无法同时高效完成“过滤 + 排序 + 分页”,会退化成先扫描所有匹配 status 的文档,再内存排序,最后跳游标——这和 skip() 没本质区别。
必须把等值字段放前、排序字段放后建复合索引,才能让一次索引扫描覆盖全部操作。
-
createdAt单字段索引:仅支持纯时间范围分页(无其他条件) - 复合索引
{ status: 1, createdAt: -1 }:能同时命中status过滤和createdAt排序,游标查询直接走索引范围扫描 - 错误顺序如
{ createdAt: -1, status: 1 }:status条件无法利用索引前缀,基本失效
db.collection.find() 游标分页语句怎么写才不丢数据?
关键不是“怎么查”,而是“怎么传游标值”。游标必须基于上一页最后一条记录的排序字段值,且方向要和 sort() 严格一致。常见错误是方向反了,或者用了非排序字段当游标。
- 正向分页(最新在前):用
{ createdAt: { $lt: lastDoc.createdAt } }+.sort({ createdAt: -1 }) - 反向分页(最早在前):用
{ createdAt: { $gt: lastDoc.createdAt } }+.sort({ createdAt: 1 }) - 绝对不要用
_id当游标去替代时间字段——除非你确认它和业务排序逻辑完全一致,否则翻页会错乱 - 如果
createdAt可能重复(比如批量导入),必须加入一个唯一字段兜底,例如{ createdAt: -1, _id: -1 },游标条件也要同步加_id
复合索引字段顺序错了会怎样?
MongoDB 复合索引遵循最左前缀原则,字段顺序一错,整个索引对分页查询就形同虚设。执行 explain("executionStats") 时你会看到 totalDocsExamined 高得离谱,甚至接近集合总文档数。
- 正确顺序:
{ userId: 1, createdAt: -1 }→ 支持{ userId: "u123" }+.sort({ createdAt: -1 }) - 错误顺序:
{ createdAt: -1, userId: 1 }→userId条件无法使用索引,触发 COLLSCAN - 多条件场景下,把高频等值字段放最左,排序字段紧随其后,分页游标字段(如
_id)放在末尾兜底
游标值怎么安全编码和传输?
前端传过来的游标值不能直接拼进查询。原始 createdAt 是 Date 对象,JSON 序列化后变成字符串,服务端解析稍有偏差就会漏数据或重复。更危险的是,如果游标里混了 ObjectId,字符串格式不统一极易出错。
- 服务端生成游标时,统一转为 ISO 字符串(
lastDoc.createdAt.toISOString()),并确保精度到毫秒 - 接收游标时,用
new Date(cursorString)解析,别依赖parseInt或自定义格式 - 涉及
_id的复合游标(防重复),建议用 Base64 编码整个游标对象,避免 URL 中出现/或+等非法字符 - 永远校验游标值是否合法——空、NaN、非法日期都应拒绝,返回 400 而不是静默失败
最容易被忽略的一点:游标分页不是“万能银弹”。它要求排序字段高度唯一、数据写入相对有序、且客户端能可靠维护游标状态。如果业务允许跳页(比如输入页码),还是得结合缓存或预计算方案,别硬扛。



















