1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel】Eloquentでシーク法ページネーション

Last updated at Posted at 2025-03-18

シーク法とは

offset用いずにページネーションをする仕組みです。
offsetを使用する方法は、sqlがシンプルな反面、データ数が大きくなるにつれて処理が重くなるというデメリットがあります。
対してシーク法は、やや複雑なsqlになりますが、データ数が大きくなっても高速で取得できるというメリットがあります。

シーク法の概要については、以下の記事が非常にわかりやすいです。
offsetでページネーションは遅い。これからはシーク法だ!

Laravelで実装

LaravelのEloquentでシーク法を実装します。
実装内容の概要は以下になります。

実装内容

  • posts:投稿記事テーブル
  • postsを投稿日時(created_at)の降順で取得する

本題のシーク法へ入る前に、まずは従来のオフセット法によるページネーションを見てみましょう。

オフセット法

従来のoffsetでのページネーションだと、以下のようになります。

PostController.php
public function index(Request $request): JsonResponse
{
    $perPage = 10; // 1ページあたりの取得件数

    // 現在のページ番号
    $page = $request->get('page');

    $posts = Post::select(['*'])
        ->orderByDesc('created_at')
        ->offset(($page - 1) * $perPage)
        ->limit($perPage)
        ->get();
        
        // 以下省略

シーク法

続いてが本題です。これをシーク法で実装すると、次のようになります。

PostController.php
    public function index(Request $request): JsonResponse
    {
        $perPage = 10; // 1ページあたりの取得件数
        
        $postQuery = Post::select(['*'])
            ->orderByDesc('created_at')
            ->orderByDesc('id')
            ->limit($perPage);

        // lastPostIdに前回取得時の最後尾のposts.idが入る
        $lastPostId = $request->get('lastPostId');
        if (isset($lastPostId)) {
            $lastPost = Post::find($lastPostId);
            $postQuery->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($lastPost) {
                $query
                    ->where('created_at', '<', $lastPost->created_at)
                    ->orWhere(function (\Illuminate\Database\Eloquent\Builder $query) use ($lastPost) {
                        $query
                            ->where('created_at', $lastPost->created_at)
                            ->where('id', '<', $lastPost->id);
                    });
            });
        }

        $posts = $postQuery->get();

        // 以下省略

これでシーク法によるページネーションが実現できました。
シーク法ではoffsetは使わず、whereでページネーションを行います。同じタイムスタンプの投稿が複数ある場合を考慮し、一意であるidでもソートする必要があります。
そのためcreated_atだけでなくidでもorderByする必要がある点や、whereorWhereなど多くてややこしいですね。
しかし一度理解してしまえば非常に有用なので、ぜひ覚えておきましょう。

なぜシーク法が早いのか?

ざっくり言うと、オフセット法の場合は100件目までスキップするイメージ、シーク法はカーソルの位置を記憶するイメージです。
オフセット法の場合はスキップ数が増えるほど処理も重くなりますが、シーク法はページ件数に関係なく取得ができるのです。

havingを用いる場合

シーク法のソート対象カラムについて、whereではなくhavingを用なければならない場合があります。(例:リレーション先のカウント数でのソートなど)
ここでは、以下の内容でhavingによるシーク法を行います。

実装内容

  • posts:投稿記事テーブル
  • post_like_users:投稿記事に「いいね」をしたユーザーを記録するテーブル
  • postsにはHasManyでpost_like_usersが紐づいている(リレーション名:likes
  • いいね数(likes_count)の降順で取得する

ソート対象のlikes_countwhereで扱うことができず、havingを用いる必要があります。
これでページネーション実装すると、以下のようになります。

PostController.php
    public function index(Request $request): JsonResponse
    {
        $perPage = 10; // 1ページあたりの取得件数
        
        $postQuery = Post::select(['*', 'posts.id AS alias_id']) // idをhavingで使用するためにalias_idとする
            ->withCount('likes')
            ->orderByDesc('likes_count') // withCount('likes')により、likes_countが使用可能
            ->orderByDesc('id')
            ->limit($perPage);

        // lastPostIdに前回取得時の最後尾のposts.idが入る
        $lastPostId = $request->get('lastPostId');
        if (isset($lastPostId)) {
            $lastPost = Post::withCount('likes')->find($lastPostId);
            $postQuery->having(function (\Illuminate\Database\Query\Builder $query) use ($lastPost) {
                $query
                    ->having('likes_count', '<', $lastPost->likes_count)
                    ->orHaving(function (\Illuminate\Database\Query\Builder $query) use ($lastPost) {
                        $query
                            ->having('likes_count', $lastPost->likes_count)
                            ->having('alias_id', '<', $lastPost->id);
                    });
            });
        }

        $posts = $postQuery->get();
        
        // 以下省略

基本的にはwhereの場合とあまり変わらないのですが、idの比較部分がwhereの場合と少し異なりますね。

->having('alias_id', '<', $lastPost->id);

whereではなくhavingを用いる必要があるので、selectで定義しておいたalias_idで行っています。

まとめ

シーク法は、offsetを使用しないことで大量データでも高速にページネーションができる手法です。
特にデータ数が多いシステムでは、オフセット法に比べてパフォーマンス面で大きなメリットがあります。

本記事では、LaravelのEloquentでの基本的なシーク法の実装から、havingを使った応用例まで紹介しました。

ポイント

  • offsetを使わず、前回取得した最後のレコード情報(カーソル)を基準に次のデータを取得する
  • created_atだけでは重複を考慮しきれないため、idでもソートする必要がある
  • ソート対象がリレーション先のカウントなどの場合は、havingを使ったシーク法が必要

シーク法を使うことで、APIの応答速度やパフォーマンスが向上するため、大規模なデータを扱う場面では積極的に取り入れていきたいですね。

告知

最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。

みなさまからのご応募をお待ちしております。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?