海量數據的分頁怎麼破?

来源:https://www.cnblogs.com/littleatp/archive/2019/07/26/11252782.html
-Advertisement-
Play Games

[TOC] 一、背景 分頁應該是極為常見的數據展現方式了,一般在數據集較大而無法在單個頁面中呈現時會採用分頁的方法。 各種前端UI組件在實現上也都會支持分頁的功能,而數據交互呈現所相應的後端系統、資料庫都對數據查詢的分頁提供了良好的支持。 以幾個流行的資料庫為例: 查詢表 t_data 第 2 頁的 ...


目錄

一、背景

分頁應該是極為常見的數據展現方式了,一般在數據集較大而無法在單個頁面中呈現時會採用分頁的方法。
各種前端UI組件在實現上也都會支持分頁的功能,而數據交互呈現所相應的後端系統、資料庫都對數據查詢的分頁提供了良好的支持。
以幾個流行的資料庫為例:

查詢表 t_data 第 2 頁的數據(假定每頁 5 條)

  • MySQL 的做法:
select * from t_data limit 5,5
  • PostGreSQL 的做法:
select * from t_data limit 5 offset 5
  • MongoDB 的做法:
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);
}

這樣看來,開發一個分頁的查詢功能是非常簡單的。
然而萬事皆不可能盡全盡美,儘管上述的資料庫、開發框架提供了基礎的分頁能力,在面對日益增長的海量數據時卻難以應對,一個明顯的問題就是查詢性能低下!
那麼,面對千萬級、億級甚至更多的數據集時,分頁功能該怎麼實現?

下麵,我以 MongoDB 作為背景來探討幾種不同的做法。

二、傳統方案

就是最常規的方案,假設 我們需要對文章 articles 這個表(集合) 進行分頁展示,一般前端會需要傳遞兩個參數:

  • 頁碼(當前是第幾頁)
  • 頁大小(每頁展示的數據個數)

按照這個做法的查詢方式,如下圖所示:

因為是希望最後創建的文章顯示在前面,這裡使用了**_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 大量數據的問題,那麼能否避免呢?答案是可以的。
改良的做法為:

  1. 選取一個唯一有序的關鍵欄位,比如 _id,作為翻頁的排序欄位;
  2. 每次翻頁時以當前頁的最後一條數據_id值作為起點,將此併入查詢條件中。

如下圖所示:

修改後的語句執行計劃如下:

{
  "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 操作實現

具體的操作如下圖所示:

實現步驟

  1. 對頁碼進行分組(groupSize=8, pageSize=20),每組為8個頁碼;

  2. 提前查詢 end_offset,同時獲得本組頁碼數量:
db.articles.find({ _id: { $lt: start_offset } }).sort({_id: -1}).skip(20*8).limit(1)
  1. 分頁數據查詢以本頁組 start_offset 作為起點,在有限的頁碼上翻頁(skip)
    由於一個分組的數據量通常很小(8*20=160),在分組內進行skip產生的代價會非常小,因此性能上可以得到保證。

小結

隨著物聯網,大數據業務的白熱化,一般企業級系統的數據量也會呈現出快速的增長。而傳統的資料庫分頁方案在海量數據場景下很難滿足性能的要求。
在本文的探討中,主要為海量數據的分頁提供了幾種常見的優化方案(以MongoDB作為實例),併在性能上做了一些對比,旨在提供一些參考。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 概覽 1.1 預定義的源和接收器 Flink內置了一些基本數據源和接收器,並且始終可用。該預定義的數據源包括文件,目錄和插socket,並從集合和迭代器攝取數據。該預定義的數據接收器支持寫入文件和標準輸入輸出及socket。 1.2 綁定連接器 連接器提供用於與各種第三方系統連接的代碼。目前支持 ...
  • MySQL的安裝和基本管理 一、MySQL介紹 MySQL是一個關係型資料庫管理系統,由瑞典MySQL AB 公司開發,目前屬於 Oracle 旗下公司。MySQL 最流行的關係型資料庫管理系統,在 WEB 應用方面MySQL是最好的 RDBMS (Relational Database Manag ...
  • 2002年~2005年我在廣州的廣東水力電力職業技術學院求學,主修網路工程。求學期間,我從事最多的就是玩游戲,當時就是玩MU和CS,所以有一門編程課叫C語言的“肥佬”(廣東話)了,要補考,沒辦法,於是我就用了整個暑假的時間來學習C語言,從此我就和編程結上了不解之緣。由於畢業設計是用ASP.NET來做 ...
  • 接著說昨天語法中提到的drop,delete和truncate的區別 drop用於刪除庫和表,不能用於刪除表記錄 delete和truncate都可以用於刪除表記錄,不能用於刪除庫和表 而delete和truncate之間的區別在: delete可以刪除表中的某一部分記錄,也可以刪除表中的所有記錄, ...
  • DDL資料庫對象管理 約束的分類: 主鍵約束:primary key 要求主鍵列數據唯一,並且不允許為空。 外鍵約束:foreign key 用於在兩表之間建立關係,需要指定引用主表的哪一列。 檢查約束:check 某列取值範圍限制、格式限制等。 例如性別列 唯一約束:unique 數據的唯一性,可 ...
  • 用戶管理 操作過程:Users->右鍵new...建用戶 用戶名和密碼: 設置許可權 1.新增用戶 -- Create the user -- tablespace表空間指數據存儲的位置 基本語法:SQL>create user 用戶名 identified by 密碼; create user st ...
  • 序列 -- sequence 序列-- 序列是資料庫的一種對象,用於生成一串不重覆的編號,可以遞增或遞減作用:可以為表中列自動產生值由用戶創建資料庫對象來創建序列(sequence),並且可以由多個用戶共用一般用在主鍵或者唯一列 1.創建序列:語法:create sequence 序列名稱start ...
  • rownum偽行號-排行榜-分頁 1.rownum 是oracle資料庫特有的一個特性,它針對每一個查詢(包括子查詢),都會生成一個rownum用於對該次查詢進行編號 2.每個rownum只針對當前select 查詢有效,可以使用別名進行顯示 例子:select rownum,emp.* from ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...