mongodb的分页

526 阅读1分钟

正文

说起分页,最容易想起的就是offset+limit,在mongodb里面可以用skip+limit实现,应该说这是最容易实现的功能,与前端的交互来说非常合理,你只需告诉前端有多少页,以及每页的大小即可。

But~~~~

这样效率太低,因为无论是哪个数据库,都会强制扫描offset或者skip的数据,即不会用到索引,所以,越到后面,skip的数据越多,效率越低。(当然了,数据量不大的话效果不明显。)

解决方案是用条件直接筛选索引,通过索引的限定直接去取数据,这样效率最高。

举个栗子:

// 已创建createdAt索引
Order
  .find({createdAt: {$gte: nextId}})
  .sort({createdAt: -1})
  .limit(limit)
  .exec()

你需要分页展示订单信息,只要前端获取nextId即可,尤其是APP,目前大部分的APP展示大量信息的时候,会选择瀑布流加无限翻滚,就是到用户翻滚到页面尾部的时候才会去请求以及加载下一页,因此不需要给前端一共多少页这样的信息,只需要告诉还有没有数据即可。

具体方案

前端第一次请求
example.com/orders
API返回order数组,以及在返回的头部信息中有x-next-id: 123456
当用户快拉到底部时,请求第二页数据
example.com/orders?next…
API返回order数组,以及在返回的头部信息中有x-next-id: 12345678
...
最后一次API返回的数组中只有2个文档,这时候头部中无x-nexd-id信息,即无更多数据

具体实现:

// 已创建createdAt索引
const orders = await Order
    .find({createdAt: {$gte: nextId}})
    .sort({createdAt: -1})
    .limit(limit + 1) //需要多取一个doc来获得nextId
    .exec();
if (docs.length === limit + 1) {
    return {
        docs: docs.slice(0, -1),
        nextId: docs[docs.length - 1]._id,
    };
}
return {
    docs,
    nextId: null,
};

需要注意的地方

不知道你发现没有,上面这个其实是可能有问题的,因为当有重复的createdAt字段存在时,就会有问题:
比如limit10个,恰好createdAt字段全部相同,于是返回的nextId跟前一次的nextId相同,那么就会造成死循环了。当然,这是最差情况,只是出现重复数据的概率挺高。

解决方案很简单,直接用unique字段,或者加上unique字段,比如id字段,query的时候用两个字段一起筛选,返回的nextId也是包含着两个字段。

MongoID不能保证连续增长,因此,在这种情况下,不适合此种方案,如果是为了保证速度以及效率的话,可以放缓存,比如redis以及memcached之类的。