NoSQL分页和传统数据库不一样
做过后端开发的都知道,分页是列表页的标配。以前用MySQL的时候,LIMIT OFFSET多简单,翻到第几页直接算一下偏移量就完事了。但到了MongoDB、Cassandra这些NoSQL数据库里,这招就不灵了。
比如你在做一个内容聚合平台,每天有上百万条动态写入MongoDB。用户刷动态时要一页一页往下拉,这时候你还用skip(10000)去跳过前一万条?那页面卡得能泡杯茶。
为什么skip会变慢
MongoDB的skip本质上是“跳过前N条记录”,它不会真的跳,而是从头开始一条条数过去。数据量越大,skip越靠后,查询就越慢。线上环境出现过因为skip(50000)导致接口响应超过5秒的情况,用户早跑了。
用游标(Cursor)代替skip
更靠谱的做法是用“游标分页”,也叫“键位分页”。核心思路是记住上一页最后一条数据的某个唯一字段值,比如时间戳或ID,下一页从那个位置往后取。
假设你的集合按创建时间倒序排列,每页10条:
db.posts.find({ createdAt: { $lt: lastSeenTime } })
.sort({ createdAt: -1 })
.limit(10)第一次查的时候不带$lt条件,拿到第一页最后一条的createdAt传给前端。前端下次请求带上这个时间点,后端作为lastSeenTime继续查。这样每次都是从索引快速定位,没有跳过过程。
注意边界情况
如果createdAt字段不是唯一的,可能会漏数据或者重复。比如同一秒插入了多条。这时候建议组合排序,比如:
db.posts.find({
$or: [
{ createdAt: { $lt: lastSeenTime } },
{
createdAt: lastSeenTime,
_id: { $lt: lastSeenId }
}
]
})
.sort({ createdAt: -1, _id: -1 })
.limit(10)这样即使时间一样,也能靠_id进一步排序,保证分页连续且不重复。
适用场景举例
你做个朋友圈类功能,用户刷动态频率高,但很少翻到几十页以后。游标分页正适合这种“往前翻”的场景。如果是后台管理系统,管理员要精确跳转到第100页,那还是得用传统方式,但数据量不大,影响有限。
另外像Elasticsearch这类搜索引擎,本身提供search_after机制,原理和游标分页类似,也是基于上一次结果的排序值进行下一页查询。
别忘了建索引
不管用哪种方式,记得在排序字段上建好索引。上面例子中,应该创建复合索引:
db.posts.createIndex({ createdAt: -1, _id: -1 })不然再好的分页策略也救不回来。
NoSQL的设计哲学本来就不强调“跳页”,而是追求高效读写。理解这点,分页思路自然就变了。