2

业务需求

用户需要根据某个时间段搜索大量历史数据(千万级别)。

设计思路

  • 基于查询条件创建索引,尽可能围绕索引构造查询语句;
  • 尽量避免使用有过滤条件的count函数统计数据总量(即使是基于索引查询),该操作非常耗时;
  • 尽量使用基于索引的范围查询代替skip操作,因为带有大偏移量的skip操作非常耗时。

具体实现

我们采用百度搜索引擎的分页设计方式,限制最大的分页数为10页,每页显示的数据条目最大为500条(页数和条数根据情况适当调整)。当用户需要获取[t1, t2]时间段内的数据时,定义一个游标变量lastTime来保存当前最新的一条数据的时间节点。那么,我们可以这样来查询需要的数据(以下代码片段只作为示例用):

const maxPage = 10; // 按照百度分页方式,跳页数不会大于10
const pageSize = 500; // 每页显示的最大数据量(如果是动态配置,需要同时更新游标)

let data; // 用来保存获取的数据
let lastTime; // 用来保存当前最新的一条数据的时间戳

// 基于时间戳创建索引
db.data.createIndex({time: 1});

// 当我们第一次加载页面时,呈现的是第一页的数据内容
data = db.data.find({time: {"$gte": t1, "$lte": t2}}).limit(pageSize).toArray();
lastTime = data[pageSize - 1].time;
show(data);

// 当用户点击下一页或者跳页(正方向)
if (skipPage == 1) { // 下一页处理,如第一页到第二页
    data = db.data.find({{time: {"$gte": lastTime, "$lte": t2}}).limit(pageSize).toArray();
} else { // 跳页处理(理论上不常用,如果用户能明确跳转几页可获取到想要的数据项,应该建议用户使用精确匹配,这样查询效率更高),如从第一页跳到第六页,此时skipPage = 5
    data = db.data.find({{time: {"$gte": lastTime, "$lte": t2}}).skip(skipPage*pageSize).limit(pageSize).toArray();
}
lastTime = data[pageSize - 1].time;
show(data);

/*
方式二,需要对返回的数据进行额外的处理,如:
data = db.data.find({{time: {"$gte": lastTime, "$lte": t2}}).limit(分页数*pageSize).toArray().slice(-pageSize);
lastTime = data[0].tmie;
show(data);
*/

// 当用户点击上一页或者跳页(反方向)
if (skipPage == 1) { // 上一页处理,如第二页到第一页
    data = db.data.find({{time: {"$gte": t1, "$lte": lastTime}}).sort({time: -1}).limit(pageSize).toArray();
} else { // 跳页处理,如从第六页跳到第一页,此时skipPage = 5
    data = db.data.find({{time: {"$gte": t1, "$lte": lastTime}}).sort({time: -1}).skip(skipPage*pageSize).limit(pageSize).toArray();
}
lastTime = data[pageSize - 1].time;
show(data);

通过上述的方式,就可以避免使用大偏移量的skip操作,也不需要使用count函数预先统计区间内的数据总量,进而提高性能。细心的你会发现,可能会存在数据重复的问题(比如,同一时刻存在多条数据,那么下一页获取的数据中可能会包含上一页的数据项)。该情况可以从两个方面来考量:1. 在时间戳的精度较高时,同一时刻的数据条目很少;2. 从使用角度看,在浏览一个页面上多条数据时,我们总是习惯会直接跳到当页最后一条数据,审查其是否符合条件,进而才会去仔细查看当页所有数据,否则会进入下页继续查找。
上述内容就是我对于mongodb在查询大量历史数据时的思考,希望能对你有帮助。如果有错误地方,欢迎指正。谢谢~


Aric
12 声望0 粉丝

咸鱼白菜也好好味~