MongoDB 游标分页方案
最常见的分页方案是基于“页码”和“页数”,后端通过计算跳过一定数量的数据记录,直接向前端返回想要的数据。
这种方案简单有效,但在数据量巨大时,由于需要调过大量数据,即使有索引可能也很慢。
如果业务上不能允许我们分库分表,可能就需要一种新的分页方案。
这里介绍一种基于游标的分页方式,理论性能不会随着数据量增长而下降。
页码分页
描述
使用 offset 和 limit 值指定每次分页跳过的数据库记录数量和每页记录数量
具体略
缺点
- 数据量大后性能差
- 当拉取过程中有新增内容,可能导致数据重复
游标分页
描述
使用数据库中自增、唯一的列(mongodb 中通常是主键 _id
)作为分页依据。客户端初次请求分页时,取 limit 数量的数据返回给前端,并将最后一条数据作为游标让前端记下来。客户端请求下一页时,必须带上刚才返回的游标。
由于游标是自增且唯一的,我们可以用简单的 greater than 查询,直接拿游标之后的数据。由于可以走主键索引,不需要耗时较长的 offset,性能比页码分页好很多,基本不会随数据量增长而性能下降。
缺点
1. 按非游标字段排序困难
给如下数据:
db.cursor_test.insertMany([
{ id: "D", recommend: 1 },
{ id: "B", recommend: 3 },
{ id: "C", recommend: 2 },
{ id: "A", recommend: 0 },
{ id: "E", recommend: 5 },
{ id: "F", recommend: 4 },
])
db.cursor_test.find({})
需求是按 recommend 顺序查询,每页 limit 值为 2,期望结果应为:
第一次查询: AD
第一次查询: CB
第一次查询: FE
实际分页查询结果:
第一次查询:
这里有个小知识点,mongodb 的字段比较是按这个文档里的说明来的
db.cursor_test.find({
id: { $gt: "0" }
})
.sort({ recommend: 1 })
.limit(2)
第二次查询(id 大于上次最后一条结果 D):
db.cursor_test.find({
id: { $gt: "0" }
})
.sort({ recommend: 1 })
.limit(2)
可见期望结果(CB)和实际结果(FE)不符。
究其原因是,实际按非 id 字段排序时,结果顺序可能和 id 顺序不符,但我们的 id gt
直接过滤掉了 id 顺序之后的数据,导致可能漏数据。
解决办法是,将排序字段和 id 同时写入查询和排序条件中,用 or
操作符包裹,以求查到同时符合游标分页和按排序字段顺序排列的数据。
下面演示查询具体代码:
第一次查询:
db.cursor_test.find({
$or: [
{ id: { $gt: "0" }, recommend: 0 },
{ recommend: { $gt: 0 } }
],
})
.sort({ recommend: 1, id: 1 })
.limit(2)
第二次查询:
db.cursor_test.find({
$or: [
{ id: { $gt: "D" }, recommend: 1 },
{ recommend: { $gt: 1 } }
],
})
.sort({ recommend: 1, id: 1 })
.limit(2)
第三次查询:
db.cursor_test.find({
$or: [
{ id: { $gt: "B" }, recommend: 3 },
{ recommend: { $gt: 3 } }
],
})
.sort({ recommend: 1, id: 1 })
.limit(2)
可见实际结果符合预期。
2. 查询条件复杂
见上文,因游标分页在存在“按非游标字段排序”的需求时,需要用 or
操作符特殊处理,可能导致查询条件相对复杂,需要谨慎处理。我们可以在外层查询条件中包裹一个 and
操作符,以方便加上其他查询条件。
示例如下:
db.cursor_test.updateMany({}, { $set: { category: 1 } })
db.cursor_test.updateOne({ id: "F" }, { $set: { category: 2 } })
db.cursor_test.find({
$and: [
{ category: 2 },
{
$or: [
{ id: { $gt: "D" }, recommend: 1 },
{ recommend: { $gt: 1 } }
]
}
]
})
.sort({ recommend: 1, id: 1 })
.limit(2)
可见查询结果符合预期。
mongo 官方文档说明过,and
操作符可以转化为隐式操作,故以上查询语句也可简化为:
db.cursor_test.find({
category: 2,
$or: [
{ id: { $gt: "D" }, recommend: 1 },
{ recommend: { $gt: 1 } }
]
})
.sort({ recommend: 1, id: 1 })
.limit(2)
效果是一样的
参考
- https://medium.com/swlh/mongodb-pagination-fast-consistent-ece2a97070f3 (内含 Python 实现)
- https://github.com/mixmaxhq/mongo-cursor-pagination