分页应该是极为常见的数据展示方式了,通常在数据集较大而没法在单个页面中呈现时会采用分页的方法。
各类前端UI组件在实现上也都会支持分页的功能,而数据交互呈现所相应的后端系统、数据库都对数据查询的分页提供了良好的支持。
以几个流行的数据库为例:数据库
查询表 t_data 第 2 页的数据(假定每页 5 条) json
select * from t_data limit 5,5
select * from t_data limit 5 offset 5
db.t_data.find().limit(5).skip(5);
尽管每种数据库的语法不尽相同,经过一些开发框架封装的接口,咱们能够不须要熟悉这些差别。如 SpringData 提供的分页接口:后端
public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { Page<T> findAll(Pageable pageable); }
这样看来,开发一个分页的查询功能是很是简单的。
然而万事皆不可能尽全尽美,尽管上述的数据库、开发框架提供了基础的分页能力,在面对日益增加的海量数据时却难以应对,一个明显的问题就是查询性能低下!
那么,面对千万级、亿级甚至更多的数据集时,分页功能该怎么实现?app
下面,我以 MongoDB 做为背景来探讨几种不一样的作法。框架
就是最常规的方案,假设 咱们须要对文章 articles 这个表(集合) 进行分页展现,通常前端会须要传递两个参数:dom
按照这个作法的查询方式,以下图所示:性能
由于是但愿最后建立的文章显示在前面,这里使用了**_id 作降序排序**。
其中红色部分语句的执行计划以下:测试
{ "queryPlanner" : { "plannerVersion" : 1, "namespace" : "appdb.articles", "indexFilterSet" : false, "parsedQuery" : { "$and" : [] }, "winningPlan" : { "stage" : "SKIP", "skipAmount" : 19960, "inputStage" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "_id" : 1 }, "indexName" : "_id_", "isMultiKey" : false, "direction" : "backward", "indexBounds" : { "_id" : [ "[MaxKey, MinKey]" ] ... }
能够看到随着页码的增大,skip 跳过的条目也会随之变大,而这个操做是经过 cursor 的迭代器来实现的,对于cpu的消耗会比较明显。
而当须要查询的数据达到千万级及以上时,会发现响应时间很是的长,可能会让你几乎没法接受!大数据
或许,假如你的机器性能不好,在数十万、百万数据量时已经会出现瓶颈
既然传统的分页方案会产生 skip 大量数据的问题,那么可否避免呢?答案是能够的。
改良的作法为:
以下图所示:
修改后的语句执行计划以下:
{ "queryPlanner" : { "plannerVersion" : 1, "namespace" : "appdb.articles", "indexFilterSet" : false, "parsedQuery" : { "_id" : { "$lt" : ObjectId("5c38291bd4c0c68658ba98c7") } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "_id" : 1 }, "indexName" : "_id_", "isMultiKey" : false, "direction" : "backward", "indexBounds" : { "_id" : [ "(ObjectId('5c38291bd4c0c68658ba98c7'), ObjectId('000000000000000000000000')]" ] ... }
能够看到,改良后的查询操做直接避免了昂贵的 skip 阶段,索引命中及扫描范围也是很是合理的!
为了对比这两种方案的性能差别,下面准备了一组测试数据。
测试方案
准备10W条数据,以每页20条的参数从前日后翻页,对比整体翻页的时间消耗
db.articles.remove({}); var count = 100000; var items = []; for(var i=1; i<=count; i++){ var item = { "title" : "论年轻人思想建设的重要性-" + i, "author" : "王小兵-" + Math.round(Math.random() * 50), "type" : "杂文-" + Math.round(Math.random() * 10) , "publishDate" : new Date(), } ; items.push(item); if(i%1000==0){ db.test.insertMany(items); print("insert", i); items = []; } }
传统翻页脚本
function turnPages(pageSize, pageTotal){ print("pageSize:", pageSize, "pageTotal", pageTotal) var t1 = new Date(); var dl = []; var currentPage = 0; //轮询翻页 while(currentPage < pageTotal){ var list = db.articles.find({}, {_id:1}).sort({_id: -1}).skip(currentPage*pageSize).limit(pageSize); dl = list.toArray(); //没有更多记录 if(dl.length == 0){ break; } currentPage ++; //printjson(dl) } var t2 = new Date(); var spendSeconds = Number((t2-t1)/1000).toFixed(2) print("turn pages: ", currentPage, "spend ", spendSeconds, ".") }
改良翻页脚本
function turnPageById(pageSize, pageTotal){ print("pageSize:", pageSize, "pageTotal", pageTotal) var t1 = new Date(); var dl = []; var currentId = 0; var currentPage = 0; while(currentPage ++ < pageTotal){ //以上一页的ID值做为起始值 var condition = currentId? {_id: {$lt: currentId}}: {}; var list = db.articles.find(condition, {_id:1}).sort({_id: -1}).limit(pageSize); dl = list.toArray(); //没有更多记录 if(dl.length == 0){ break; } //记录最后一条数据的ID currentId = dl[dl.length-1]._id; } var t2 = new Date(); var spendSeconds = Number((t2-t1)/1000).toFixed(2) print("turn pages: ", currentPage, "spend ", spendSeconds, ".") }
以100、500、1000、3000页数的样本进行实测,结果以下:
可见,当页数越大(数据量越大)时,改良的翻页效果提高越明显!
这种分页方案其实采用的就是时间轴(TImeLine)的模式,实际应用场景也很是的广,好比Twitter、微博、朋友圈动态均可采用这样的方式。
而同时除了上述的数据库以外,HBase、ElastiSearch 在Range Query的实现上也支持这种模式。
时间轴(TimeLine)的模式一般是作成“加载更多”、上下翻页这样的形式,但没法自由的选择某个页码。
那么为了实现页码分页,同时也避免传统方案带来的 skip 性能问题,咱们能够采起一种折中的方案。
这里参考Google搜索结果页做为说明:
一般在数据量很是大的状况下,页码也会有不少,因而能够采用页码分组的方式。
以一段页码做为一组,每一组内数据的翻页采用ID 偏移量 + 少许的 skip 操做实现
具体的操做以下图所示:
实现步骤
对页码进行分组(groupSize=8, pageSize=20),每组为8个页码;
db.articles.find({ _id: { $lt: start_offset } }).sort({_id: -1}).skip(20*8).limit(1)
随着物联网,大数据业务的白热化,通常企业级系统的数据量也会呈现出快速的增加。而传统的数据库分页方案在海量数据场景下很难知足性能的要求。 在本文的探讨中,主要为海量数据的分页提供了几种常见的优化方案(以MongoDB做为实例),并在性能上作了一些对比,旨在提供一些参考。