webシステムやサイトで最もよく使うページネーション処理はLaravelでPagenateするよりも効率的なPagination処理ができるよという話

2022年3月11日

テクノロジー

eyecatch どっちを選ぶ?と言われたら「負荷の少ない方」と答える、ユゲタです。 え?ポテトチップスと、チョコレートのどっちがいいか?だって?バックエンドサーバー処理の話かと思ったよ・・・ こんな会話を毎日行なっている、アホまっしぐらの自分ですが、今お手伝いしている会社でLaravelを扱っていて、サイト内で扱うページネーションを一括処理で行う方法を今回サーバー負荷を考慮して自作したという話です。

ページネーション知らない人のため

この記事を読んでいる人は、おそらくタイトルのLarabelとページネーションがGoogle検索でヒットしたか、どこかの記事まとめサイトなどで見かけて、閲覧している、 Laravelで仕事をしている人または、Laravelの学習がてらに自分のサイトなどを構築している人ではないでしょうか? もちろん「ページネーション」という言葉を知らないワケはないと思いますが、簡単に説明すると、 一覧リストを表示する時に、たくさんのリストを表示するよりも、1ページ内に10個や20個程度のリスト表示をして、その個数ごとにページを割り振って、◯ページ目にダイレクト遷移できるようにする機能です。 フロントエンジニアの人もUIなどを工夫するために、構築には、ライブラリを使う人も多いと思いますが、Laravelは、簡単な記述で実装ができるというメリットがあります。(ユゲタはUI機能は使わないですけどね) 今回はあくまでバックエンドとしてのデータ処理として、ページネーションに必要な情報をデータとして返す処理を行なっています。 ちなみに、インターフェイスとしては、GraphQLを使って、Json形式で変換するようになっています。

Laravel標準機能のpagenateがイケてない点

SQLベタ書きだろうが、クエリービルダーを使って記述しようが、$query->get()とするところを、$query->pagenate(15)とすれば、 簡単にページ内に15件表示する、ページネーションに必要な情報が取得できます。 ページネーションに必要な情報はどういうものがあるかと言うと、必要最低限の情報は次の3つです。
  1. ページ内に表示する件数
  2. データ検索した結果総数
  3. 1ページ内ひ表示するデータ一式
これをSQLでやる場合は、 SELECT * FROM list_table LIMIT 10 OFFSET 20 このようになって、list_tableというテーブルから、10個のデータをリストピックアップして、その3ページ目を取得するサンプルSQLです。 ページ内に10件表示という事で、OFFSETの値は、1ページ目が0、2ページ目が10、3ページ目が20となる計算です。 LIMIT値が指定してあれば、簡単にOFFSETの計算はできますよね。 Laravelのpagenate機能を使うと、SLQの検索結果から、データヒットしたTotal数やら、該当のURLリンク、検索結果が大量だった場合のcursor処理(万単位未満であれば、cursor処理は気にしなくて大丈夫です)などが取得できます。 でも、これって、LaravelがSQLを実行するときに、まず普通にSQL実行してTOTALの値を取得。 次に、LIMITとOFFSETをセットした状態でデータ取得を行って、データ部分とTOTAL数をガッチャンコさせてデータを返しているという内部構造のようです。 そうなんですね。SQLが2回実行されているようです。 なんか1回で済むだけのSQL実行が2回になるって、そのページがトップページにあったとしたら、サーバー負荷が簡単に2倍に膨れ上がってしまうという単純計算になってしまいます。

PostgreSQLで1回処理で済ませる方法

今回、仕事で行なっている環境は、PostgreSQLを使っていたので、どうすればいいかを簡単い説明すると、上記のSQLを次のように変更します。 SELECT * , count(*) over() as pagination_total FROM list_table LIMIT 10 OFFSET 20 SELECT部に", count(*) over() as pagination_total"が追加すると、検索結果の全てのレコードに、"pagination_total"というカラムが追加されて、そこに、limitしなかった場合のTOTAL値が入ってきます。 ※MySQLなど、他のSQLは別の書き方になるので、お気をつけください。 このデータを、次のように整形するといいでしょう。 [ "data" => 検索結果のレコードリスト, "pagenation" => [ "total" => 検索結果総数, "page" => 現在ページ, "perPage" => ページ内に表示する件数, "from" => ページ内アイテムのそう検索数に対する最初に表示する番号, "to" => ページ内アイテムのそう検索数に対する最後に表示される番号, "count" => ページ内に表示されるアイテム件数※基本的にperPageと同じ(最終ページのみ、perPageと違う場合があります) ] ] これをTrait処理に記述して、どのModelからでも呼び出しができるようにして、見事に共通仕様が完成して、ホッコリ顔のユゲタでした。 いや〜、webシステムに貢献した感覚で満足満足!

最後に

今回紹介したSQL技ですが、色々検証していたところ、めちゃくちゃ件数が多いテーブルに対して実行すると、逆に遅くなることがあるので、量の少ないボリュームの時だけ使うようにしてください。 Laravelがわざわざ共通仕様で、SQL2回実行をやっているのは、そのためなんだね。 という事で、仕事などでこうしたお手軽処理を書いて、サーバー負荷が激上がりした場合、クレームを言ってくるのはお断りしています。 負荷検証なども自分の責任において、プログラミングしてくださいませ。