Laravelでページネーションを実装する際、paginate() と並んでよく使われるのが simplePaginate() です。
1. ページネーションが必要な理由
データベースに数万件のデータがある場合、一度に全件を取得して画面に表示しようとすると以下の問題が発生します。
- サーバー負荷: 大量のメモリを消費する
- レンダリング遅延: ブラウザの表示が重くなる
- 通信量: ユーザーのデータ通信量を無駄に消費する
これらを解決するために、データを「1〜10件」「11〜20件」と分割して取得するのがページネーションです。
2. simplePaginate() の仕組み:内部クエリの正体
simplePaginate(10) を実行したとき、Laravelは内部で以下のような処理を行っています。
+1件取得のロジック
最大のポイントは、「指定した件数(perPage)よりも1件多くデータを取得しにいく」という点です。
例えば、1ページ10件表示の場合、SQLは以下のようになります。
- 内部で発行されるSQLのイメージ
SELECT FROM projects
LIMIT 11 OFFSET 0; - 10件ではなく「11件」取得している
次ページの判定フロー
なぜ11件取得するのか?それは「次のページがあるか」を判断するためです。
-
11件取得できた場合:
- 11件目は表示せず、「次ページあり」と判定する
-
next_page_urlに次のページのURLをセットする
-
10件以下しか取得できなかった場合:
- 取得した分だけ表示し、「次ページなし」と判定する
-
next_page_urlはnullになる
このように、「実際にデータを取ってみて、余りがあるかで次を確認する」のが simplePaginate の賢いところです。
3. なぜ paginate() より軽いのか
標準の paginate() との決定的な違いは、「総件数を数えるクエリ(COUNT)を発行するかどうか」にあります。
-
paginate():
-
SELECT COUNT(*) ...(全件数を数える) -
SELECT * ... LIMIT 10 OFFSET 0(データ取得)
-
-
simplePaginate():
-
SELECT * ... LIMIT 11 OFFSET 0(データ取得 + 次の確認)
-
データ量が100万件を超えるような巨大なテーブルでは、COUNT(*) は非常に重い処理になります。simplePaginate はこのカウントをスキップするため、劇的に高速になります。
4. OFFSET(オフセット)の概念
ページが進むにつれて、Laravelは OFFSET を調整します。
| ページ | OFFSET | 内部的な動き |
|---|---|---|
| 1ページ目 | 0 | 飛ばさずに取得 |
| 2ページ目 | 10 | 最初の10件を読み飛ばして、11件取得 |
| 3ページ目 | 20 | 最初の20件を読み飛ばして、11件取得 |
5. 実装上の注意点
① orderBy(並び替え)は必須
リレーショナルデータベースでは、並び替えを指定しないとデータの返却順序が保証されません。ページを切り替えるたびに順序が変わると、同じデータが重複して表示される原因になります。
// 推奨:作成日順やID順で固定する
Project::query()->latest()->simplePaginate(10);
② 大量データ時の「OFFSET」問題
simplePaginate は高速ですが、弱点もあります。それは「深いページ(後ろの方のページ)」です。
OFFSET 99990 のようなクエリになると、DBは最初の99,990件をスキャンして捨てるという作業を行うため、パフォーマンスが低下します。
-
解決策: さらに高速化が必要な場合や、数百万件の深層ページを扱う場合は、OFFSETを使わない
cursorPaginate()を検討してください
6. 向いているユースケース
simplePaginate は以下のようなUIを実装する際に最適です。
- 「さらに表示」ボタン: 総件数がわからなくても成立するUI
- 簡易的な一覧画面: 「次へ / 前へ」のナビゲーションだけで十分な場合
- 無限スクロール(簡易版): スクロールに合わせて次の10件を結合していく実装
7. 返却値(JSON)の構造と意味
simplePaginate() が返すレスポンスには、データ本体だけでなく、フロントエンドが「次ページがあるか」などを判断するための情報が含まれています。
{
"current_page": 2,
"data": [ ... ],
"first_page_url": "https://api.example.com/v1/projects?sort=newest&status=active&page=1",
"from": 11,
"next_page_url": "https://api.example.com/v1/projects?sort=newest&status=active&page=3",
"path": "https://api.example.com/v1/projects",
"per_page": 10,
"prev_page_url": "https://api.example.com/v1/projects?sort=newest&status=active&page=1",
"to": 20
}
返却値の各項目(プロパティ)解説
-
current_page- 現在表示しているページ番号です
- 「今、全データのうちの何枚目のカードを見ているか」を示します
-
data- 取得したメインのデータ本体です
- 指定した件数(例:10件)のレコードが配列形式で格納されます
-
first_page_url- 最初のページ(page=1)に直接戻るためのURLです
-
from- 現在のページに表示されているデータの「開始番号」です
- (例:2ページ目で10件ずつの場合、11番目から始まるので
11)
-
next_page_url- 次のページを取得するためのフルパスURL(例:
https://api.example.com/v1/projects?page=2)が格納されます - 次ページがない場合は
nullになる
- 次のページを取得するためのフルパスURL(例:
-
path- ページ番号などのパラメータを除いた、ベースとなるAPIのエンドポイントURLです
-
per_page- 1ページあたりの表示件数です
-
simplePaginate(10)で指定した引数の値がそのまま入ります
-
prev_page_url- 前のページに戻るためのURLです
- 1ページ目を表示している場合は
nullになります
-
to- 現在のページに表示されているデータの「終了番号」です
- (例:2ページ目で10件ずつの場合、20番目で終わるので
20)