分頁應該是極爲常見的數據展示方式了,通常在數據集較大而沒法在單個頁面中呈現時會採用分頁的方法。
各類前端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做爲實例),並在性能上作了一些對比,旨在提供一些參考。