見出し画像

オフセット・ページネーションとカーソル・ページネーションの比較

Twitterでこの新しくLaravelに追加されるカーソル・ページネーションについての記事を見つけて面白かったので、自分なりにまとめてみたいと思います。

オフセット・ページネーション

現在オフセット・ページネーションはよく使われているページネーションの一つです。

LaravelpaginatesimplePaginateのメソッドはこのオフセット・ページネーションをデフォルトで使用しています。

次のユーザーのテーブルのデータのページネーションを考えてみましょう。

use App\Models\User;

$users = User::orderBy('id')->simplePaginate(20);

オフセット・ページネーションは offset句を使ったクエリを使用します。

例えば2ページ目へアクセスした時に実行されるクエリを見てみましょう。

select * from users order by `id` asc limit 20 offset 20;

カーソル・ページネーション

カーソル・ページネーションはパフォーマンスに優れているページネーションの方法でデータの数が多い時によく使われ、Infinite Scroll (無限スクロール)やAPIなどで使用されることもあります。Laravelに新しく追加されるcursorPaginateメソッドはこのカーソル・ページネーションを使用しています。

オフセット・ページネーションの時と同じように、カーソル・ページネーションを使ったユーザーのテーブルのデータのページネーションを見てみましょう。

use App\Models\User;

$users = User::orderBy('id')->cursorPaginate(20);

simplePaginateメソッドの時と同じですが、クエリが異なってきます。

今回も2ページ目のデータ取得のクエリを見てみます。

select * from users where `id` > 20 order by `id` asc limit 20;

クエリを見てみるとわかりますが、オフセット・ページネーションとカーソル・ページネーションの違いはオフセット・ページネーションはoffset句を使用し、カーソル・ページネーションは比較オペレーターを使用したwhere句を使用している点です。

パフォーマンス

これはShopifyのエンジニアの調査でわかったことらしいのですが、カーソル・ページネーションオフセット・ページネーションの400倍のパフォーマンスを出せるようです。ただし、カーソル・ページネーションのクエリのorder byで使用されるカラムはインデックスを貼る必要があります。

パフォーマンスで圧倒的な差が生まれる理由は、オフセット・ページネーションのクエリが実行されるとき、すべてのデータがスキャンされることが理由にあげられます。例えば offset 100,000 だとするとデータベースはその100,000の行をスキャンする必要があります。一方、カーソル・ページネーションのクエリは 

where `id` > 100,000

の場合は100,000の行まで(一つ一つスキャンせず)跳ばして辿り着けるのでクエリの実行が速くなります。(インデックスが正確に貼られている時に限る)

重複・不足データ

オフセット・ページネーションでよく起こる問題の一つに重複、もしくは不足データです。これは特に書き込みが頻繁に行われるデータセットの時に起こりやすく、前のページのデータが追加もしくは削除された時に起こります。

offset句は基本的にその数だけ行がスキップされるので、ページの表示前に前のページのデータに追加されると、重複されたデータが表示され、前のページのデータが削除された場合には、その分だけのデータが不足して表示されます。これが理由でオフセット・ページネーションは書き込みが頻繁に行われるデータには適切では無いですが、カーソル・ページネーションはwhere id > 10のようなwhereをクエリで使用しているためデータが重複したり不足したりするようなことは起きません。

このため無限スクロールなどでoffsetを使用してしまうと、ずっと同じデータが出力されると言うことも起きてしまいます。

カーソル・ページネーションの制限

カーソル・ページネーションがオフセット・ページネーションと比べて優れている点を記述しましたが、すべてにおいて優れているわけではありません。何点か見ていきましょう。

ページ番号

カーソル・ページネーションはページ番号を表示するページネーションはサポートしていません。 『前』と『次』のページネーションは対応しています。もしページ番号を使う必要があるならオフセット・ページネーションを使用しましょう。

データベースインデックス

カーソル・ページネーションは order by で使用するカラムにインデックスを貼る必要があります。つまり、サイト上の表示で複数のソートのオプションがある場合、複数のカラムにインデックスを貼る必要がでてきます。

この記事でも書いたように、インデックスの数が増えると書き込みのパフォーマンスは落ちてきます。

ユニーク

カーソル・ページネーションのwhere句の条件で使われるカラムはユニークである必要があります。

select * from users where `name` > 'tanaka' order by `name` asc limit 100;

もしユーザーのテーブルの名前(name)が条件で使われているとしたら、nameはユニークではないので、カーソル・ページネーションは成り立ちません。

次のようなクエリを作成することでカーソル・ページネーションで使用することができます。

select * from users where (`name`, `id`) > ('tanaka', 100) order by `name` asc, `id` asc limit 100;

最後に

すべてにとって良いページネーションなどありません。小さいデータセットのページネーションを実装したい場合、またはページ番号の表示が必要な場合はオフセット・ページネーションを使用するべきですが、書き込みの多いデータセットの場合や無限スクロールの場合はカーソル・ページネーションを使用すると良いです。自分の環境に合わせて選んでください。



この記事が気に入ったらサポートをしてみませんか?