TL;DR
- ページ番号(オフセット)によるページネーションは、ページが深くなるほど遅くなる。
- 前のページの最後のレコードを覚えて続きから取る相対カーソルなら、どのページも同じくらいの速さで取れる。Shopify は API をこの方式に移行した。
内容
オフセットの問題
外部のアプリは商品カタログ全体を同期することが多く、10 万件を超える商品を持つショップもある。オフセットを使うクエリは、それより前の行をすべて読んで捨てるので、オフセットが大きいほど遅くなる。1,400 万件の商品を持つテスト用のショップでは、オフセット 10 で約 6.5 ms だったクエリが、オフセット 100,000 では 2,221.60 ms かかり、1,000,000 以上ではタイムアウトした。遅いクエリはデータベースの接続を占有するので、同じデータベースを使う他のショップにも影響する。
相対カーソルによるページネーション
各リクエストは前のページの最後のレコード(たいていは id)の続きから取る。id で並べれば主キーのインデックスが使えるので、オフセット 100,000 に当たるクエリも 5.24 ms で済んだ。代わりに、任意のページへ直接移動することはできなくなる。
並べ替えとレコードの取りこぼし
id 以外(たとえばタイトル)で並べるときは、カーソルにそのフィールドの最後の値を持たせる。並べ替えのフィールドが一意でないと、その値だけで絞り込むとレコードを取りこぼすので、id のような一意な列を第 2 の並べ替えキーにし、両方の値をカーソルに入れる。この 2 つの列の複合インデックスがないと、ページ番号より遅くなることもある。タイトルと id で並べると id だけより遅いが、オフセット 100,000 でも 6.00 ms で、オフセットよりはるかに速い。
クライアントの負担を減らす
並べ替えのフィールドがレスポンスに含まれないこともあるので、クライアントにカーソルを計算させず、次と前のページの URL を Link ヘッダで返す。
Shopify での移行
REST API にバージョニングを入れたばかりだったので、ページ番号を廃止できるようになった。ページネーションを担当するチームはなかったが、商品の API を持つ著者のチームが提案を書き、共通のコードと移行ガイドを用意して、各チームが担当する API を移行した。/admin/products.json では、相対カーソルのリクエストはページ番号より平均で約 11 倍速かった。2020 年 7 月以降、どのバージョンの API もページ番号をサポートしない。
キーワード
- ページネーション
- オフセット
- カーソル
- インデックス
- Link ヘッダ