API 分页探讨:offset 来分页真的有效率吗?
翻譯:高可用架構(ArchNotes)
對于設計和實現 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?msExecution?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?msExecution?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/
HackerNews 評論:
https://news.ycombinator.com/item?id=25547716
HN網友 et1337:
使用游標的另一個原因是避免由于并發編輯而導致元素重復或跳過的問題,比如你使用 offset 正在第 10 頁上,而有人在第 1 頁上刪除了一個項目,則整個列表會移動,你可能會意外跳過第 11 頁上的一行數據。同樣,如果有人在第 1 頁上添加了一條記錄而你正在第 10 頁上,第 10 頁中的一項也會重復顯示在第 11 頁上。
游標優雅地回避了這些問題。
HN 網友 chrismorgan:
有時候,你需要一個游標,這樣你就可以從你剛才的地方繼續前進,而不用擔心新的記錄進來擾亂你的分頁。
有時你想要基于位置的查詢,因為你明確地希望所有的東西都是位置的。
有時你想把這兩種技術結合起來,例如,如果你跳到一個大的、不斷變化的列表中間,然后想在剛才的位置之后檢索下一批結果。
我喜歡 JMAP 最后的設計(https://tools.ietf.org/html/rfc8620#page-45):你可以指定一個位置整數,或者一個錨 ID 和可選的 anchorOffset 整數。錨是游標的一種實現,它使用結果集中一個實體 ID,而不是一個可以嵌入其他信息(比如 coroutine 地址)的不透明類型,,它有一個明顯的優點,就是可以由客戶端控制。
HN 網友?vincnetas
我認為作者在使用 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
HN 網友 boulos
鑒于對“游標”一詞的重用感到困惑,我更喜歡 Google 為分頁所使用的術語:頁面令牌和頁面大小,詳細可以參閱:
https://google.aip.dev/158
圖片推薦文章2021年 我辭職了!
2020年國內互聯網公司的薪酬排名!
955 互聯網公司白名單來了!這些公司月薪20k,沒有996!福利榜國內大廠只有這家!
寫博客能月入10K?
一款基于 Spring Boot 的現代化社區(論壇/問答/社交網絡/博客)
這或許是最美的Vue+Element開源后臺管理UI
推薦一款高顏值的 Spring Boot 快速開發框架
一款基于 Spring Boot 的現代化社區(論壇/問答/社交網絡/博客)
13K點贊都基于 Vue+Spring 前后端分離管理系統ELAdmin,大愛
想接私活時薪再翻一倍,建議根據這幾個開源的SpringBoot項目
總結
以上是生活随笔為你收集整理的API 分页探讨:offset 来分页真的有效率吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用pytorch的相关问题总结
- 下一篇: 推荐3个快速开发平台 前后端都有 项目经