開発者からよく同じ質問を受けます。「APIが遅いのですが、何から始めればいいですか?」 反射的にサーバーをアップグレードしたり、RAMを2倍にしたりしたくなりますが、それにはコストがかかる上に、根本的な原因が解決されることはめったにありません。ほとんどのLaravelアプリケーションにおいて、ボトルネックはデータベース層にあります。フレームワークの洗練された構文のせいで、Eloquentの呼び出しが最終的にSQLになること、そしてそのSQLこそが問題の起点であることが忘れられがちです。

サーバーの設定に手を付ける前に、クエリを体系的に見直しましょう。

正しい診断から始める

暗闇の中で最適化をしてはいけません。場当たり的にクエリを書き換えるのは推測に過ぎず、その推測は時間を浪費させます。

合計で最も時間を消費しているステートメントを見つける必要があります。実際の負荷がかかっている状態のアプリケーションを観察してください。Laravel Telescopeを使えば、リクエスト中に実行されたすべてのクエリを、実行時間とともに分かりやすく確認できます。ローカル開発中であれば、Laravel Debugbarがブラウザ上にそれらを表示してくれるため、異常をすぐに察知できます。本番環境で問題を捉える必要がある場合は、MySQLのSlow Query Logを有効にしてください。これは定義したしきい値を超えるステートメントを記録するため、小規模なデータセットでは現れない予期せぬ問題を見つけるのに最適です。より大規模な運用を行っている場合は、APM(Application Performance Monitoring)ツールを使用して、遅いHTTPエンドポイントと特定のデータベース呼び出しを紐付けることができます。

データを確認する際は、2つの点に注目してください。「絶対的な実行時間」と「呼び出し頻度」です。40ミリ秒かかるクエリは一見無害に思えますが、それが1分間に2,000回実行されていると気づいたとき、話は変わります。1時間に1回実行される3秒かかるレポートよりも、全ページで実行される0.5秒のルックアップの方が重要かもしれません。影響度の高い問題から優先的に解決しましょう。

すべてを取得するのをやめる

SELECT * は便利ですが、コストも高いです。Model::all() を書いたり、カラムを指定せずに結果セットを取得したりすると、MySQLは一致したすべての行に対してすべてのフィールドを読み込みます。これには、大きなテキストフィールドやJSON Blob、その他テーブルにあるあらゆるデータが含まれます。その結果、結果セットが肥大化し、メモリ使用量が増加し、レスポンスのシリアライズに費やす時間が増えてしまいます。

明示的に指定しましょう。コントローラーに idnameemail フィールドだけが必要な場合は、それらだけを要求してください。

User::select('id', 'name', 'email')->get();

クエリビルダーでも同じ原則が適用されます。ペイロードが小さければネットワークをより速く移動でき、アプリケーションサーバーのRAM消費も抑えられます。これは最も手軽に成果を得られる方法の一つですが、Laravelが SELECT * をデフォルトの挙動としているため、つい見落としがちです。

EXPLAIN を変更の指針にする

まず EXPLAIN を実行せずに、遅いクエリをリファクタリングしてはいけません。MySQLにおいて、EXPLAIN キーワードはクエリの実行計画を表示します。これにより、オプティマイザがどのようにデータを検索しようとしているのかが正確に分かります。

type カラムに注目してください。もし ALL と表示されていれば、MySQLはフルテーブルスキャンを実行しています。つまり、WHERE 句を満たすためにすべての行を読み取っていることを意味します。key カラムを見て、オプティマイザがインデックスを実際に使用しているかを確認してください。次に Extra カラムを確認します。Using temporaryUsing filesort が見つかった場合、現在の構造ではクエリを効率的に処理できないため、MySQLが中間テーブルを作成したり、メモリ内でソートを行ったりしています。

MySQLクライアントで EXPLAIN を実行するか、出力を整形してくれるツールを使用してください。実行計画さえ分かれば、問題がインデックスの欠如なのか、不適切な結合(join)なのか、あるいはエンジンが最適化できない述語(predicate)なのかが判明します。もはや推測は不要です。

意図を持ってインデックスを貼る

インデックスはルックアップを高速化するための最も強力なツールですが、クエリの書き方と一致している場合にのみ機能します。適切なインデックスがないと、MySQLは行を一つずつスキャンします。1,000行程度のテーブルを扱う開発環境では問題なく感じられても、1,000万行のテーブルを扱う本番環境では崩壊する可能性があります。

まずは WHERE 句に頻繁に登場するフィールドに対して、単一カラムのインデックスを作成することから始めましょう。常に status でフィルタリングしているなら、status にインデックスを追加します。

複数のカラムを組み合わせてフィルタリングする場合は、複合インデックス(composite indexes)に移行します。MySQLは複合インデックスを左から右へと読み取るため、インデックス内のカラムの順序が重要になります。これは「最左プレフィックス(leftmost prefix)ルール」と呼ばれます。もしクエリが user_id で検索し、その後に created_at でソートする場合、(user_id, created_at) の複合インデックスは非常に効果的です。順序を逆にすると、オプティマイザはフィルタリングにそのインデックスを全く使用しない可能性があります。

すべてのカラムにインデックスを貼ってはいけません。インデックスを追加するたびに、MySQLはその構造を維持する必要があるため、挿入(insert)、更新(update)、削除(delete)のオーバーヘッドが増加します。測定中に見つけたパターンに基づいて、意図的に追加してください。

カラムに関数を適用しない

このミスは、気づかないうちにインデックスを無効化してしまいます。WHERE 句の中でカラムを関数で囲むと、MySQLは...