今天有人問我要怎麼做分頁會比較好? 我很直覺的就說了直接用 offset + limit 就好啦! 我心裡想難道還有其他的方式嗎?

這個時候想起之前在接 Facebook 和 Google 的 API 的時候,似乎有個叫 cursor 跟 nextPage 之類的東西,於是花了點時間研究了一下,並把心得記錄下來~

目前主要的 pagination 也就分為兩大類

  1. Page number pagination 這種就是最常見的底下有一排數字的分頁方式,就像 google search 頁面下方那種。

  2. Infinate pagination 這種就是 facebook 或是 twitter 可以一直往下滑的,我稱為無限捲軸的分頁。

以上兩種不同的分頁方法也就對應到兩種實作的方式,每個方法都有其優缺點和限制,就看你的需求再來決定要採取哪種分頁的實作,下面介紹兩種對應的實作方法。

Offset based Pagination

第一種就是最直覺的 offset + limit,給一個 offset 決定從哪開始再加上一個 limit 看是要拿多少個,是個閉著眼睛都想得到的方式XD

這種實作通常是對應到 Page number pagination ,常用於列舉產品項目或是檔案列表這類型的應用,像是想要列出前50筆檔案:

GET /files?offset=0&limit=50

或是列出前50個產品項目:

SELECT * FROM products OFFSET 0 LIMIT 50;

在用 SQL 的 OFFSETLIMIT 要注意效能問題, query 的效率會隨著 offset 越大而越差,因為這個 query 還是會把全部 record 選出來,然後 skip offset 之前的 record,所以才會說 offset 越大效能越差,這篇 解釋的不錯。

另外 offset 最大的問題還是下面這兩點

  1. 會漏掉資料

  2. 會列出重複的項目

舉個例子,假如現在有 10 個項目,然後一個分頁是 10 個,那麼當你加了一個新項目時,原本的第 10 個就變成了第 11 個,你切到 page 2 的時候就會重複列出了 page 1 的最後一個項目,而且你也會漏掉了剛新加入的東西,除非你有再切回 page 1 啦。

但如果你的應用是可以接受上面的缺點,那麼 offset based pagination 是個簡單易懂的方法。

Cursor based Pagination

第二種方法就是利用一個 cursor 來當作指標, 當你想要更多東西的時候,就可以給個 cursor 說明我這次要從這個地方往後拿,差異在於這個 cursor 是個絕對位置,像是 timestamp 或是以遞增方式產生的 post id,以 Facebook API 為例

"paging": {
    "cursors": {
      "after": "MTAxNTExOTQ1MjAwNzI5NDE=",
      "before": "NDMyNzQyODI3OTQw"
    },
    "previous": "https://graph.facebook.com/me/albums?limit=25&before=NDMyNzQyODI3OTQw"
    "next": "https://graph.facebook.com/me/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
  }

這樣一來就不怕會漏訊息或是列出重複的東西,但是他就無法像 offset based pagination 那樣直接切換到某一頁。

另外 cursor pagination 也無法支援任意欄位的排序,因為 cursor 只能用有絕對位置屬性的欄位,舉個例子假如你想實作一個檔案列表 API pagination 並且需要支援排序名稱,那你就無法使用 cursor pagination 了,為什麼呢? 假設你的 cursor = “aaa.txt” ,你沒有辦法保證新增的檔案會在 “aaa.txt” 之後,這樣一來就跟 offset pagination出現一樣會漏掉的狀況!

Summary

這兩種大概是目前主流的 pagination 方式,至於要採用哪一種要看自己程式的需求了,大致上可以這樣分

  1. Real time data pagination => cursor based pagination
  2. Static data pagination 或是需要支援多欄位排序 => offset based pagination

Reference