【Laravel】ページネーション(paginate)の使い方:simplePaginate・cursorPaginateとの違いと、links()の表示・日本語化
目次
- 試すために用意した posts テーブルと23件のデータ
- paginate() で一覧とページ番号を出す
- simplePaginate() は「前へ」「次へ」だけ
- cursorPaginate() はページ番号の代わりに cursor を使う
- 3つのメソッドの違い
- API なら paginator をそのまま return する
- 2ページ目で検索条件が消えるときは withQueryString()
- 取るカラムやページを引数で指定する
- ページ番号の数を onEachSide() で減らす
- 「Showing 6 to 10 of 23 results」を日本語にする
- ページ送りの矢印が画面いっぱいに出るときは CSS が読まれていない
- 参考: ビューを書き出して自分で直す・ページネーターを2つ置く
- まとめ
Laravelのpaginate・simplePaginate・cursorPaginateの使い方と違いを、Laravel 13で実際に発行されるSQL・JSON・links()の画面で確かめました。2ページ目で検索条件が消えるときのwithQueryString、ページネーションの日本語化、矢印が巨大になるときの直し方も載せています。
一覧ページで「5件ずつ表示して、下にページ番号を出したい」というときに使うのが、Laravel のページネーションです。
Laravel では paginate() を1つ書くだけで、件数の計算・LIMIT と OFFSET・ページ番号のリンクまで作ってくれます。
ただ、似たメソッドが paginate()・simplePaginate()・cursorPaginate() の3つあって、どれを使えばいいのか迷いますよね?
この記事では、3つのメソッドで実際にどんな SQL が発行されるのか、JSON や画面がどう変わるのかを、Laravel 13 で確かめました!
確かめた環境は、Docker の php:8.4-cli(PHP 8.4.27)・Composer 2.11.2・Laravel 13.42.0(SQLite)、CSS のビルドは node:22(Node 22.23.5)、画面の確認は Chromium 156 です(2026年11月16日)。
composer create-project laravel/laravel で作ったままのプロジェクトに、posts テーブルを1つ足して試しています。
試すために用意した posts テーブルと23件のデータ
posts テーブルは、タイトルとカテゴリーだけの簡単なものです。
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('category');
$table->timestamps();
});シーダーで23件入れました。
偶数の記事が laravel、奇数の記事が react というカテゴリーです。
foreach (range(1, 23) as $i) {
Post::create([
'title' => "記事{$i}",
'category' => $i % 2 === 0 ? 'laravel' : 'react',
]);
}23件を5件ずつに分けるので、最後のページは5ページ目(3件)になります。
paginate() で一覧とページ番号を出す
まずは一番よく使う paginate() です。
routes/web.php に、5件ずつ取って一覧のビューに渡すルートを書きました。
use App\Models\Post;
use Illuminate\Support\Facades\Route;
Route::get('/posts', function () {
$posts = Post::orderBy('id')->paginate(5);
return view('posts', ['posts' => $posts]);
});paginate() の引数は、1ページに表示する件数です。
ビューでは、いつもの @foreach で回したあとに {{ $posts->links() }} と書くだけです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>記事一覧</title>
@vite(['resources/css/app.css'])
</head>
<body class="p-8">
<h1 class="text-xl font-bold mb-4">記事一覧</h1>
<ul class="mb-6">
@foreach ($posts as $post)
<li>{{ $post->id }}: {{ $post->title }}({{ $post->category }})</li>
@endforeach
</ul>
{{ $posts->links() }}
</body>
</html>npm run build で CSS をビルドしてから /posts?page=2 を開くと、6〜10件目と、ページ番号のリンクが出ました。
左の「Showing 6 to 10 of 23 results」も、右のボタンも、links() が出したものです。
画面の幅を 400px にすると、ページ番号と件数が消えて「Previous」「Next」の2つのボタンだけになりました。
スマホ向けの表示も最初から入っているので、自分で作らなくて大丈夫です。
何ページ目かは ?page= で決まる
どのページを出すかは、URL の ?page=2 の数字で決まります。
コントローラーで page を受け取る処理は書いていませんが、paginate() がリクエストから自動で読んでいます。
ありえない値を入れたときの動きも見てみました。
| URL | 結果 |
|---|---|
/posts?page=2 | 6〜10件目 |
/posts?page=99 | エラーにならず、データが空(current_page は 99) |
/posts?page=abc | 1ページ目 |
/posts?page=-1 | 1ページ目 |
最後のページを超えた数字を入れても 404 にはならず、空の一覧が出ます。
「存在しないページは 404 にしたい」ときは、$posts->isEmpty() などで自分で判定する必要がありそうです。
paginate() は SQL を2回発行する
DB::enableQueryLog() で、paginate(5) が発行した SQL を見てみました。
select count(*) as "aggregate" from "posts"
select * from "posts" order by "id" asc limit 5 offset 01回目で全体の件数を数え、2回目で5件だけ取っています。
3ページ目(?page=3)では、2回目が limit 5 offset 10 になりました。
全体の件数が分かるので、「全23件」「最後は5ページ目」のような表示ができるわけです。
simplePaginate() は「前へ」「次へ」だけ
paginate() を simplePaginate() に変えると、ページ番号が消えて「Previous」「Next」だけになりました。
$posts = Post::orderBy('id')->simplePaginate(5);発行された SQL は1回だけでした。
select * from "posts" order by "id" asc limit 6 offset 05件ずつなのに limit 6 になっているのがポイントです。
1件多く取ってみて、6件目があれば「次のページがある」と判断しているようです。
件数を数える count(*) がないので、全体の件数と最後のページ番号は分かりません。
データが多くて count(*) が重いときや、「もっと見る」のような画面なら、こちらで十分です。
cursorPaginate() はページ番号の代わりに cursor を使う
3つ目の cursorPaginate() は、URL に page ではなく cursor という文字列が付きます。
$posts = Post::orderBy('id')->cursorPaginate(5);1ページ目の JSON(後で書く「API で返す」と同じ方法)の、data 以外の部分はこうなりました。
{
"path": "http://localhost:8000/api/posts-cursor",
"per_page": 5,
"next_cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"next_page_url": "http://localhost:8000/api/posts-cursor?cursor=eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"prev_cursor": null,
"prev_page_url": null
}この cursor を base64 でデコードすると、中身はただの JSON でした。
{"id":5,"_pointsToNextItems":true}「id が 5 の次から」という意味です。
この cursor を付けて2ページ目を取ったときの SQL がこちらです。
select * from "posts" where ("id" > ?) order by "id" asc limit 6バインドされた値は 5 でした。
OFFSET を使わず where id > 5 で続きを取っているので、何ページ進んでも、前のページの行を読み飛ばす必要がありません。
その代わり、ページ番号は出せず、画面の表示は simplePaginate と同じ「Previous」「Next」だけになります。
ちなみに、orderBy() を書かずに Post::cursorPaginate(5) としてみたところ、エラーにはならず、SQL に order by "posts"."id" asc が自動で付いていました。
orderByDesc('id') にすると order by "id" desc になります。
3つのメソッドの違い
確かめた結果をまとめるとこうなります。
| paginate | simplePaginate | cursorPaginate | |
|---|---|---|---|
| SQL の回数 | 2回(count と本体) | 1回 | 1回 |
| 続きの取り方 | OFFSET | OFFSET | WHERE id > 前の最後の値 |
| URL | ?page=2 | ?page=2 | ?cursor=eyJ... |
| ページ番号のリンク | 出る | 出ない | 出ない |
| 全件数(total)・最後のページ | 分かる | 分からない | 分からない |
迷ったら、普通の一覧ページは paginate() で大丈夫です。
件数がとても多いテーブルや、無限スクロールのような画面では simplePaginate() や cursorPaginate() を考える、という使い分けになります。
API なら paginator をそのまま return する
API で JSON を返したいときは、paginator をそのまま return するだけで JSON になりました。
Route::get('/api/posts', fn () => Post::orderBy('id')->paginate(5));/api/posts?page=2 の結果です(data は1件目だけ残し、links は途中を省略しています)。
{
"current_page": 2,
"data": [
{
"id": 6,
"title": "記事6",
"category": "laravel",
"created_at": "2026-11-15T19:08:14.000000Z",
"updated_at": "2026-11-15T19:08:14.000000Z"
}
],
"first_page_url": "http://localhost:8000/api/posts?page=1",
"from": 6,
"last_page": 5,
"last_page_url": "http://localhost:8000/api/posts?page=5",
"links": [
{
"url": "http://localhost:8000/api/posts?page=1",
"label": "« Previous",
"page": 1,
"active": false
},
{
"url": "http://localhost:8000/api/posts?page=2",
"label": "2",
"page": 2,
"active": true
},
{
"url": "http://localhost:8000/api/posts?page=3",
"label": "Next »",
"page": 3,
"active": false
}
],
"next_page_url": "http://localhost:8000/api/posts?page=3",
"path": "http://localhost:8000/api/posts",
"per_page": 5,
"prev_page_url": "http://localhost:8000/api/posts?page=1",
"to": 10,
"total": 23
}links には、ページ番号のボタンを作るための情報が全部入っています。
Vue や React で自分でページ番号を並べるときは、この links を回せば作れます。
simplePaginate() の JSON には total・last_page・links がなく、代わりに current_page_url が入っていました。
2ページ目で検索条件が消えるときは withQueryString()
カテゴリーで絞り込んだ一覧にページネーションを付けると、ハマりやすいところがあります。
Route::get('/posts', function (Request $request) {
$query = Post::orderBy('id');
if ($request->filled('category')) {
$query->where('category', $request->category);
}
$posts = $query->paginate(5);
return view('posts', ['posts' => $posts]);
});/posts?category=laravel を開くと、1ページ目は laravel の記事だけが並びます。
ところが、links() が作ったリンクはこうなっていました。
http://localhost:8000/posts?page=2
http://localhost:8000/posts?page=3category=laravel が付いていません。
このリンクで2ページ目に進むと、絞り込みが外れて react の記事も混ざった6〜10件目が出ます。
withQueryString() を付けると、今の URL のクエリ文字列がそのままリンクに引き継がれました。
$posts = Post::where('category', $request->category)
->orderBy('id')
->paginate(5)
->withQueryString();/posts-qs?category=laravel&sort=new で開いたときのリンクです。
http://localhost:8000/posts-qs?category=laravel&sort=new&page=2
http://localhost:8000/posts-qs?category=laravel&sort=new&page=3引き継ぐものを選びたいときは、appends() で指定します。
->paginate(5)
->appends(['category' => $request->category]);同じ URL で開くと、sort=new は付かず category だけが付きました。
http://localhost:8000/posts-appends?category=laravel&page=2
http://localhost:8000/posts-appends?category=laravel&page=3取るカラムやページを引数で指定する
paginate() には、件数のほかにページも渡せます。
Post::select('id', 'title')->orderBy('id')->paginate(perPage: 3, page: 2);この書き方で ?page=5 を付けて開いたところ、URL の5ではなく、引数の2ページ目(4〜6件目)が返ってきました。
引数で page を渡すと、URL の page より優先されます。
select() で絞ったカラムだけが data に入り、total は23のままでした。
ページ番号の数を onEachSide() で減らす
ページ数が多いと、links() は途中を「...」で省略します。
2件ずつ(12ページ)にして、onEachSide(1) を付けて6ページ目を開いてみました。
$posts = Post::orderBy('id')->paginate(2)->onEachSide(1);今のページの左右に1つずつ(5と7)、両端に2つずつが出て、間は「...」になりました。
ページ番号のボタンの数を抑えたいときは、この数を小さくします(^^)
「Showing 6 to 10 of 23 results」を日本語にする
links() の文字は英語なので、日本語にしたいですよね?
「Previous」「Next」と「Showing」などの文字は、置き場所が別でした。
まず、言語ファイルを出します。
php artisan lang:publishlang/en/pagination.php ができて、中に「Previous」「Next」が入っていました。
'previous' => '« Previous',
'next' => 'Next »',これをコピーして lang/ja/pagination.php を作ります。
<?php
return [
'previous' => '« 前へ',
'next' => '次へ »',
];「Showing」「to」「of」「results」は、lang/ja.json に書きます。
ビューの中で {!! __('Showing') !!} のように1語ずつ訳されていて、並び順は「Showing 6 to 10 of 23 results」のまま変えられません。
そこで、この順番のまま日本語として読めるようにしました。
{
"Showing": "表示中:",
"to": "〜",
"of": "件目(全",
"results": "件)"
}最後に、.env の APP_LOCALE を ja にします。
APP_LOCALE=ja「表示中: 6 〜 10 件目(全 23 件)」になりました!
「Showing」を空文字 "" にして消せるかも試しましたが、空にすると訳されず「Showing」のまま出ました。
消したいときは、次の参考に書いたビューの書き出しで、ビューのほうを直すことになりそうです。
ページ送りの矢印が画面いっぱいに出るときは CSS が読まれていない
links() の HTML は Tailwind CSS 用です。
@vite(['resources/css/app.css']) の行を外して開くと、こうなりました。
矢印の SVG に大きさを決める CSS が当たらず、画面の幅いっぱいに広がっています(;'∀')
「ページネーションの矢印が巨大になる」ときは、たいていこれです。
今の Laravel の resources/css/app.css には、ページネーションのビューを Tailwind の対象にする行が最初から入っていました。
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';なので、Tailwind を使っているなら、レイアウトに @vite(['resources/css/app.css']) を書いて npm run build(開発中は npm run dev)をすれば直ります。
Vite で「manifest が見つからない」と出るときは、Vite manifest not found の解決方法 を見てください。
Bootstrap を使っているなら useBootstrapFive()
Tailwind ではなく Bootstrap を使っているときは、Bootstrap 用のビューに切り替えます。
tinker で Paginator::useBootstrapFive() を呼んでから links() を出してみたところ、class="pagination"・page-item・page-link という Bootstrap のクラスの HTML に変わりました。
use Illuminate\Pagination\Paginator;
Paginator::useBootstrapFive();参考: ビューを書き出して自分で直す・ページネーターを2つ置く
- 公式ドキュメントでは、
Paginator::useBootstrapFive()をApp\Providers\AppServiceProviderのboot()の中で呼ぶ形が紹介されています。Bootstrap 4 用のuseBootstrapFour()もあるそうです。 - ビューの HTML を自分で直したいときは、
php artisan vendor:publish --tag=laravel-paginationでresources/views/vendor/paginationに書き出せるとのことです。既定のビューは、その中のtailwind.blade.phpだそうです。 - 1つの画面にページネーションを2つ置くと、どちらも
?page=を使うのでぶつかります。paginate(15, ['*'], 'users')のように3つ目の引数で名前を変えられるとのことです。 cursorPaginate()は、並べ替えに使うカラムがユニーク(または組み合わせでユニーク)である必要があり、null を含むカラムは使えないとされています。- URL のパスを変える
withPath('/admin/users')や、#usersを付けるfragment('users')もあるようです。
出典: Database: Pagination(Laravel 13.x 公式ドキュメント)
まとめ
- 普通の一覧は
paginate()。count と本体の2回の SQL で、ページ番号まで出せます。 - 件数が要らないなら
simplePaginate()。1件多く取って次があるかを見るので、SQL は1回です。 - 件数が多い・無限スクロールなら
cursorPaginate()。OFFSET を使わずwhere id > ?で続きを取ります。 - 検索条件を2ページ目に引き継ぐには
withQueryString()かappends()。 - 日本語化は
lang/ja/pagination.phpとlang/ja.json、矢印が巨大なら CSS のビルドを確かめます。
一覧を取る Eloquent の書き方は Laravel Eloquentの使い方、一覧を @foreach で表示するところは Laravel で foreach を使って一覧を作成する方法、絞り込みの条件は Laravel で where 複数で AND 検索をする方法 にも書いています。
union した結果に paginate() を付けたときの件数の数え方は、unionで違うテーブル同士を一緒に取得する方法 で確かめました。
誰かの参考になれば幸いです!




