mongodb学习笔记--分页查询优化

  有时候我们在非关系数据库mongodb做一些简单的分析查询,比如分页。mongodb本身提供了分页的api,但是比较有限。

1、分页api

mongodb自身提供了类似mysql的分页关键字:

查询时

1
query.skip((pageNum - 1)*pageSize).limit(pageSize);

需要是跳跃查询,但是达到一定数据量级的,查询效率很低,甚至查不动。

skip达到一定数据量时,超过了系统默认的32MB内存排序,所以mongo重新使用了其他算法排序,出现大量扫描,导致慢查询。

这个写法和mysql的limit offset,rows类似:

下面两条语句是等价的。

1
2
3
select id from collect limit 1000000,10;
#**MySQL5.0之后支持该语法**
select id from collect limit 10 offset 1000000;

等于是一直往后读取到第1000000条,开始取10条,读取了1000000没用的数据,mysql的优化方式是找到第1000000条数据的id,开始读取10条,主要是利用索引位置来定位分页起始位:

1
select id from collect where id>1000000 limit 10;

查询过程经过了pageNum*pageSize条数据。

1
2
3
4
5
6
7
graph LR
web-->pageNum
web-->pageSize
pageNum-->pageNum*pageSize
pageSize-->pageNum*pageSize
pageNum*pageSize-->skip
skip-->limit第pageNum页的数据

2、优化方向

等官方优化skip()确定时间,毕竟mongo使用场景也大多不是用来分页,那么只能避免使用skip()。不使用skip()那怎么优化呢?我们借助其他函数曲线救国,比如sort()排序和 limit() ,和mysql的优化方向一样,知道起始id再查询当前页,理论上会快很多。相当于跳过了pageNum*pageSize条数据。

1
2
3
4
5
6
7
8
9
graph LR
web-->pageNum
web-->pageSize
pageNum-->pageNum*pageSize的数据_id
pageSize-->pageNum*pageSize的数据_id
pageNum*pageSize的数据_id-->使用$gt函数大于_id
pageNum*pageSize的数据_id-->skip跳页pageSize
使用$gt函数大于_id-->第pageNum页的数据
skip跳页pageSize-->第pageNum页的数据

3、实现

对象中设置id属性即可得到_id的值,String的话得到一串十六进制的字符构成的字符串,具体构成可查看ObjectId的构成。

假设实体对象为Goods,设置id属性:

1
private String id;

将得到_id设置为分页的起始_id,,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public List<Goods> getListByPage(int pageNum, int pageSize) {
List<Goods> goodsList;

Query query = new Query();

// 通过 _id 正序排序
query.with(Sort.by(Sort.Direction.ASC, "_id"));

if (pageNum != 1) {
// number 参数是为了查上一页的最后一条数据
int number = (pageNum - 1) * pageSize;
query.limit(number);

List<Goods> goodsListTemp = mongoTemplate.find(query, Goods.class);
// 取出最后一条
Goods goods = goodsListTemp.get(goodsListTemp.size() - 1);

// 取到上一页的最后一条数据_id,当作条件查接下来的数据
String id = goods.getId();

// 从上一页最后一条开始查(大于不包括这一条)
query.addCriteria(Criteria.where("_id").gt(id));
}
// 页大小重新赋值,覆盖 number 参数
query.limit(pageSize);
// 即可得到第n页数据
goodsList = mongoTemplate.find(query, Goods.class);

return goodsList;
}

这样的避免了skip()的使用,通过sort()排序和 limit() 限制数据大小结合排序,每一次分页查询从数据的上一页的最后一条数据作为起始位置,再查询页大小的数据量。

4、建议

从代码表现上来看,每一次查询都需要通过查询当前页之前的所有数据来得到起始位置的_id,相当于查询了大量数据,对内存消耗较大。

针对页数不是特别多的情况下,这种优化方式也是比skip()效率要高的。

如果页数特别多,每页size也较大,那么不管是什么类型的数据库压力也是比较大的,这种情况就需要从业务方面考量,不适合做分页,或者将分析型业务独立,这样的分析统计类操作可以放到Elasticsearch等更适合的存储上。

作者

wonderomg

发布于

2020-09-13

更新于

2022-01-14

许可协议

评论