API 分頁設計與實現探討

2021-02-14 Java後端

對於設計和實現 API 來說,當結果集包含成千上萬條記錄時,返回一個查詢的所有結果可能是一個挑戰,它給伺服器、客戶端和網絡帶來了不必要的壓力,於是就有了分頁的功能。

通常我們通過一個 offset 偏移量或者頁碼來進行分頁,然後通過 API 實現類似請求:
GET /api/products?page=10{"items": [...100 products]}

如果要繼續訪問後續數據,則修改分頁參數即可。

GET /api/products?page=11{"items": [...another 100 products]}

在使用 offset 的情況下,通常使用 ?offset=1000 和 ?offset=1100 這種大家都熟悉的方法。它要麼直接調用 OFFSET 1000 LIMIT 100 的 SQL 查詢資料庫,要麼使用 LIMIT 乘以 page 作為查詢參數。無論如何,這是一個次優的解決方案,因為無論哪種資料庫都要跳過前面 offset 指定的 1000 行。而跳過額外的offset,不管是 PostgreSQL,ElasticSearch還是 MongoDB 都存在額外開銷,資料庫需要對它們進行排序,計數,然後將前面不用的數據扔掉。

這是一種低效的方法,但由於它使用簡單,所以大家重複地用這個方法,也就是直接把 API 參數映射到資料庫查詢上。那合適的方法是什麼?介紹之前我們可以先看看資料庫的實現。在資料庫中有一個遊標(cursor)的概念,它是一個指向行的指針,然後可以告訴資料庫:"在這個遊標之後返回 100 行"。這個指令對資料庫來說很容易,因為你很有可能通過一個索引欄位來識別這一行。然後就不需要去取和跳過前面那些沒用到的記錄了。

舉個例子。

GET /api/products{"items": [...100 products], "cursor": "qWe"}

API 返回一個無業務意義的字符串(遊標),你可以用它來檢索下一個頁面。

GET /api/products?cursor=qWe{"items": [...100 products], "cursor": "qWr"}

實現遊標有很多方法。一般來說,可以通過一些排序欄位比如產品 id 來實現。在這種情況下,你可以用一些可逆算法對產品 id 進行編碼。而在接收到一個帶有遊標的請求時,你會對它進行解碼,並生成一個類似 WHERE id > :cursor LIMIT 100 的查詢。下面是一個小小的性能對比,先看看 offset 是如何工作:
=# explain analyze select id from product offset 10000 limit 100;                                                           QUERY PLAN                                                            ---- Limit  (cost=1114.26..1125.40 rows=100 width=4) (actual time=39.431..39.561 rows=100 loops=1)   ->  Seq Scan on product  (cost=0.00..1274406.22 rows=11437243 width=4) (actual time=0.015..39.123 rows=10100 loops=1) Planning Time: 0.117 ms Execution Time: 39.589 ms

再看看 where (cursor) 語句如何工作:

=# explain analyze select id from product where id > 10000 limit 100;                                                          QUERY PLAN                                                          - Limit  (cost=0.00..11.40 rows=100 width=4) (actual time=0.016..0.067 rows=100 loops=1)   ->  Seq Scan on product  (cost=0.00..1302999.32 rows=11429082 width=4) (actual time=0.015..0.052 rows=100 loops=1)         Filter: (id > 10000) Planning Time: 0.164 ms Execution Time: 0.094 ms

這是幾個數量級的差異! 當然,實際的差異取決於表的大小以及過濾器和存儲的實現。有一篇不錯的文章 (1) 提供了更多的技術信息,裡面有 ppt,性能比較見第 42 張幻燈片。
(1) https://use-the-index-luke.com/no-offset當然,用戶不會按 id 來檢索商品,而是會按一些相關性來查詢(然後按 id 作為關聯欄位)。在現實世界中,需要根據你的業務來決定該怎麼做。訂單可以按 id 排序(因為它是單調增加的)。購買清單可以按 wishlist 時間排序。在我們的案例中,產品來自 ElasticSearch,自然支持遊標的特性。我們可以看到的一個不足是,使用無狀態的 API, 無法支持翻到「上一頁」這樣的功能。所以在面向用戶界面中,如果有 prev/next 或者 「直接進入第10頁」 這樣的按鈕,就沒有辦法繞過前面提到的 offset/limit 這種實現。但是在其他情況下,使用基於遊標的分頁可以極大地提高性能,特別是在真正的大表和真正的深度分頁上。https://solovyov.net/blog/2020/api-pagination-design/https://news.ycombinator.com/item?id=25547716使用遊標的另一個原因是避免由於並發編輯而導致元素重複或跳過的問題,比如你使用 offset 正在第 10 頁上,而有人在第 1 頁上刪除了一個項目,則整個列表會移動,你可能會意外跳過第 11 頁上的一行數據。同樣,如果有人在第 1 頁上添加了一條記錄而你正在第 10 頁上,第 10 頁中的一項也會重複顯示在第 11 頁上。有時候,你需要一個遊標,這樣你就可以從你剛才的地方繼續前進,而不用擔心新的記錄進來擾亂你的分頁。有時你想要基於位置的查詢,因為你明確地希望所有的東西都是位置的。有時你想把這兩種技術結合起來,例如,如果你跳到一個大的、不斷變化的列表中間,然後想在剛才的位置之後檢索下一批結果。我喜歡 JMAP 最後的設計(https://tools.ietf.org/html/rfc8620#page-45):你可以指定一個位置整數,或者一個錨 ID 和可選的 anchorOffset 整數。錨是遊標的一種實現,它使用結果集中一個實體 ID,而不是一個可以嵌入其他信息(比如 coroutine 地址)的不透明類型,,它有一個明顯的優點,就是可以由客戶端控制。我認為作者在使用 OFFSET 時忽略了一些關鍵點。至少 postgres 文檔對此有明確的的說法(https://www.postgresql.org/docs/13/queries-limit.html)When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. You might be asking for the tenth through twentieth rows, but tenth through twentieth in what ordering?看起來作者提供的分頁查詢沒有考慮到排序,這意味著第 100 頁上的項目的 ID 大於 10000,但順序未定義。explain analyze select id from product where id > 10000 limit 100鑑於對「遊標」一詞的重用感到困惑,我更喜歡 Google 為分頁所使用的術語:頁面令牌和頁面大小,詳細可以參閱:https://google.aip.dev/158

公眾號運營至今,離不開小夥伴們的支持。為了給小夥伴們提供一個互相交流的平臺,特地開通了官方交流群。掃描下方二維碼備註 進群 或者關注公眾號 Java後端 後獲取進群通道。

相關焦點

  • 結合 Bootstrap + Vue 組件實現 Laravel 異步分頁功能
    在日常開發中,對資料庫查詢結果進行分頁也是一個非常常見的需求,我們可以基於之前介紹的查詢方法和前端 HTML 視圖實現分頁功能,不過從 Laravel 5.3 開始,Laravel 框架就已經為我們提供了非常完整的分頁解決方案,包括後端 API 和前端視圖。
  • 開發出優秀的API,構建RESTful API的13種最佳實踐,學會此文就很優秀了
    設計RESTful API的最佳實踐是什麼?從理論上講,任何人都可以在不到五分鐘的時間內快速啟動數據API——無論是Node.js,Golang還是Python。我們將探討在構建RESTful API時應考慮的13種最佳實踐。但首先,讓我們快速闡明RESTful API。
  • Rocket-API 版本更新,基於 Spring Boot 的 API 敏捷開發框架
    Rocket-API 2.2.3 版本發布了,本次更新內容包括: 修復 groovy 引擎重複創建引起的內存溢出問題 處理大小寫轉換 修改擴展自定義分頁時異常問題 處理 mongo 下 findAll 返回數據最多 101 條記錄問題 處理 #{${}} 變量值篏套問題 db.count() 計數優化 添加全局變量 Utils 中的 pasreToString, pasreToObject 方法來實現對象與 string 的轉換概述"Rocket-API
  • Django實現分頁功能
    本節要講的分頁功能大家一定不陌生,就像課本上的一篇篇課文一樣,如果課文內容很多就會分成很多頁,展示給讀者。這和我們在開發階段處理數據信息是一樣的,因為大多數情況下,我們往往會面對很多的數據信息,為了讓這些信息顯示的更便於閱讀以及減小數據的提取量從而減少伺服器的壓力等,我們就會採用分頁的處理方法,Django 為開發者提供了內置的模塊 Paginator 類。
  • 分頁功能的分析與設計
    編輯導語:我們在網頁上瀏覽內容時,劃到最下面時經常需要進行翻下一頁查看新的內容,也可以選擇跳轉到其他頁數;讓我們在瀏覽信息是更加清晰,以免當前頁太多信息造成混亂;本文作者詳細介紹了分頁功能的分析與設計,我們一起來看一下。
  • Rocket-API 2.3.0.RELEASE,API 敏捷開發框架
    更多信息查看:開啟頁面配置功能多數據源配置rocket-api-platform 數據接口平臺軟體介紹:通過約定的方式 實現統一的標準。告別加班,拒絕重複勞動,遠離搬磚概述"Rocket-API" 基於spring boot 的API敏捷開發框架,服務端50%以上的功能只需要寫SQL或者 mongodb原始執行腳本就能完成開發,另外30%也在不停的完善公共組件,比如文件上傳,下載,導出,預覽,分頁等等通過一二行代碼也能完成開發,剩下的20%也能依賴於動態編譯技術生成class的形式,不需要發布部署,不需要重啟來實現研發團隊的快速編碼
  • 資料庫分頁查詢的幾種實現思路
    當系統中的數據超出一定數量時,給展示端展示列表性的數據時,一般不會把所有的數據一次性全部顯示到展示端,體驗良好的互動設計一般是一次只展示一部分數據,通過上下翻頁或指定頁碼的方式查看其它頁的數據,就像翻書一樣。另一方面當數據量大時,伺服器的資源也限制了一次查所有數據,如果一次查詢的數據量過多,資料庫和應用伺服器的內存都有可能被撐爆。
  • 「ThinkPHP5開發連載76」tp5連載雜項之分頁-分頁實現
    上一篇文章講解「模型-內置標籤之定義標籤」,本篇文章講解「雜項-分頁之分頁實現」。一、分頁實現ThinkPHP5.1內置了分頁實現,要給數據添加分頁輸出功能變得非常簡單。1.使用Db類實現分頁1)使用Db類查詢的時候調用paginate方法:①新建Index控制器,並新建dbpage方法②新建dbpage.html模板,並在模板中展示數據預覽:
  • REST API設計優秀實踐之參數與查詢的使用
    在此,我們以分頁為例,即:如果某個資料庫中存放著上百萬篇的文章,那麼我們很可能無法在單次響應中,將每一篇文章都發送給客戶。針對此類需求,業界提出了參數化(parametrization)的解決方法。什麼是參數化?通常而言,參數化是一種請求配置。在程式語言中,我們可以通過某個函數來請求對應的返回值。
  • REST API URI 設計 7 準則
    在今天的網站上,URI 設計範圍從可以清楚地傳達API的資源模型,如:http://api.example.com/louvre/leonardo-da-vinci/mona-lisa到那些難以讓人理解的,比如:http://api.example.com/68dd0-a9d3-11e0-9f1c-0800200c9a66Tim Berners-Lee
  • RESTful API 設計規範
    在端點的設計中,你 必須 遵守下列約定:來看一個反例:https://api.example.com/getUserInfo?userid=1https://api.example.com/getusershttps://api.example.com/sv/uhttps://api.example.com/cgi-bin/users/get_user.php?
  • 使用SpringData JPA 實現分頁
    本文公眾號來源:PandaJava  作者:panda-java本文由讀者投稿,這篇文章主要講解了使用SpringDataJPA如何實現分頁。之前我寫過兩篇SpringData JPA搭建的文章,但沒寫過分頁(前兩篇)使用SpringData JPA  實現分頁環境: Eclipse Mars.2 + JDK 1.8 + Gradle 3.5 + thymeleaf 3首先我們前臺html把分頁菜單導航欄弄出來。用bootstrap的分頁插件。
  • Angularjs+servlet+mysql實現表格分頁
    上一篇文章小編講解了如何實現表格。在Javaweb開發中,最常用到的是表格分頁,今天小編就講解一下如何實現表格分頁。一、分頁原理。以student表為例。前端頁面實現表格分頁,後端資料庫使用的查詢語句select * from student limit 參數1,參數2,參數1代表從第幾個值開始查詢,即記錄起始索引值,參數2代表查詢幾個值,即每頁顯示多少條記錄假如起始索引從0開始,每頁顯示2條記錄規律:
  • 局部刷新如何實現?看我簡單實現局部刷新、分頁
    比如我們在做菜品管理,上下翻頁時,我們的分類以及其他的信息一般是不需要刷新,只需要更新我們的菜品信息即可,如果跳轉後臺會浪費很長時間,如果我們使用ajax實現異步刷新。就可以在html中通過js對頁面進行簡單的控制實現局部刷新的效果。我們今天就以分頁查詢為例進行講解!!!
  • @創交互 如何設計體面的分頁控制項
    視覺君今天跟大家一起來分享的是,互動設計師技能之分頁控制項的設計。只要是有list pages就會遇到分頁的設計。很多情況下,設計師和開發者在產品上線後才認真考慮分頁設計。事實上,分頁是很容易就設計的,但是,不是大多數網站和app都有一個體面的分頁。
  • UX設計細節:無限滾動VS分頁
    本文共 2566 個字,預計閱讀 7 分鐘,記得點擊上面的 藍字 關注我哦~在頁面內容過多時,很多設計師不清楚應該選擇無限滾動還是分頁設計?
  • MySQL分頁優化解析
    似乎討論分頁的人很少,難道大家都沉迷於limit m,n?在有索引的情況下,limit m,n速度足夠,可是在複雜條件搜索時,where somthing order by somefield+somefieldmysql會搜遍資料庫,找出「所有」符合條件的記錄,然後取出m,n條記錄。
  • PHP中的數組分頁實現(非資料庫)
    PHP中的數組分頁實現(非資料庫)在日常開發的業務環境中,我們一般都會使用 MySQL 語句來實現分頁的功能。但是,往往也有些數據並不多,或者只是獲取 PHP 中定義的一些數組數據時需要分頁的功能。這時,我們其實不需要每次都去查詢資料庫,可以在一次查詢中把所有的數據取出來,然後在 PHP 的代碼層面進行分頁功能的實現。
  • 在Smartbi分頁報表設置裡,該怎麼實現報表分頁展示?
    分頁報表作用:用於將數據進行分頁展現。 適用場景:適用數據量較大的報表。 分頁報表是以分頁的形式展現報表數據的一種報表功能。
  • WPS Excel:怎樣實現按組分頁列印
    分類匯總這個功能除了可以實現按類別分組匯總數據,還可以實現按組分頁列印。具體來說,只需要以下兩個步驟。第 1 步點擊表格中任意一個單元格,切換到「數據」菜單下,點擊「分類匯總」,選中「每組數據分頁」,設置好分類欄位、匯總方式和匯總項。分類欄位就是你想要拆分的組別名稱,匯總項就是你要計算的欄位,匯總方式就是你想計算總和(求和)還是獲得總計數。