メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
LIMIT 20 OFFSET 200000 は 200,020 行を読み、そのうち 200,000 行を捨てます。これはバグではなく、OFFSET の意味そのものです。サーバーには任意の並び順で 200,001 行目に直接ジャンプする方法がないため、そこまで歩いて行きます。1 ページ目は速く、10,000 ページ目は遅く、その遅さはページの深さに比例して増えます。キーセットページネーションは、先頭から数えるのではなく、直前のページがどこで終わったかを覚えておくことでこれを解決します。

OFFSET ページネーション: 問題が出るまでは問題ない

おなじみのパターンです。
これは浅いページングには問題ありません。管理画面や、人間が 20 ページ目より先までスクロールすることのない結果セットなどです。問題は深いページで現れます。
  • コストがページ番号とともに増える。 OFFSET 200000 は、何かを返す前に 200,000 件のインデックスエントリをスキャンして捨てます。
  • リクエストの間に行がずれる。 ユーザーがページをめくっている間に行が挿入されると、ページの境界が動き、ユーザーは重複を見るか、行を見逃します。

キーセットページネーション: 止まった場所を覚える

行を数える代わりに、クライアントが最後に見た値で絞り込みます。
WHERE id > 20 句によって、サーバーはインデックス内を直接シークし、ページの深さに関係なくちょうど 20 行だけを読めます。10,000 ページ目のコストは 1 ページ目と同じです。境界は位置ではなく値なので、挿入された行がページの境界をずらすこともありません。 トレードオフ: キーセットページネーションが提供するのは「次へ」と「前へ」であり、「47 ページ目へジャンプ」ではありません。無限スクロールや API では、たいていそれが本来望んでいたインターフェースです。

キー以外で並べ替える

実際の一覧が id で並ぶことはまれです。一意でない列で並べ替える場合はタイブレーカーが必要です。そうしないと、同じ値を持つ行がページをまたいでスキップされたり繰り返されたりします。クライアントは最後に見た行の両方の値を送り返し、クエリは両方で比較します。
SQL には、WHERE (created_at, id) > ('2026-08-01 12:00:00', 143) という、より短い行コンストラクタの書き方もあります。同じ行を返しますが、MySQL のオプティマイザはこれをインデックスの範囲シークに変換しません。EXPLAIN は、行コンストラクタに対してはフルスキャン(クエリがインデックスでカバーされるかどうかに応じて type: ALL または type: index)を、上に示した展開形に対してはシーク(type: range)を示します。展開形を使用してください。 どちらの場合も、インデックスは ORDER BY と一致していなければなりません。
EXPLAIN でプランを確認してください。そのインデックスに対する type: range が表示され、filesort がないことが望ましい状態です。何を見るべきかについては、EXPLAIN の読み方を参照してください。

総件数は必要ですか?

大きな絞り込み済みセットに対する SELECT COUNT(*) は、ページクエリ自体よりもコストがかかることがあります。キーセットページネーションの上に構築されたインターフェースは、たいてい正確な件数をやめて「さらに読み込む」という操作に置き換えます。プロダクトが本当に件数を必要とする場合は、リクエストごとに数えるのではなく、キャッシュするか集計テーブルで管理してください。

どちらを使うか

関連項目