Laravelのコードレビューで遅いSQLの候補を洗い出す
作成日:2026.10.02
Laravelのコードレビューで、ループ内のDBアクセスやEloquentのN+1、大量取得、検索条件とインデックスなど、性能問題につながりやすい箇所を候補として洗い出します。Laravelで発行SQLと実行時間を確認し、MySQLのEXPLAINや実データに近い環境での計測へつなげる方法を紹介します。コードレビューだけではSQLの実行時間を断定できない点も説明します。
目次
LaravelのPull Requestをレビューしていると、「この処理はデータが増えたら遅くならないだろうか」と気になることがあります。ループの中でModelを検索していたり、一覧を取得した後にリレーションへアクセスしていたりするコードは、SQLの実行回数が増える候補です。
ただし、コードを見ただけで「このSQLは遅い」とは断定できません。実行時間や負荷は、テーブルの件数、インデックス、検索条件、DBの状態などで変わります。コードレビューでは問題になりそうな箇所を見つけ、発行SQLや実行計画、実データに近い環境での計測へつなげます。
SQL一回の重さだけで判断しない
SQLの実行時間が短くても、同じリクエスト内で何度も発行されれば、処理全体に影響します。逆に、複雑に見えるSQLでも、対象データが少なければ問題にならないことがあります。
レビューでは、次の要素を合わせて見ます。
- SQL一回あたりの処理と、呼び出される回数
- 検索・更新するデータの件数
- 同じ処理がリクエストやバッチで実行される頻度
たとえば、ループの中で一件ずつDBを検索するコードは、入力件数に応じてSQLの回数も増える可能性があります。単独のSQLだけでなく、呼び出し元のループや処理全体を追うことが大切です。
ループ内の検索・更新を見る
CSVや外部APIから受け取ったデータを処理するとき、ループ内で毎回Modelを検索していないか確認します。
foreach ($records as $record) {
$employee = Employee::where('employee_code', $record['code'])->first();
if ($employee !== null) {
$employee->update([
'name' => $record['name'],
]);
}
}
この例では、レコードごとにSELECTを実行し、対象が見つかればUPDATEも実行します。1件あたりのSQLが軽くても、入力が数万件になればDBとの往復が増えます。
レビューでは、まずループの件数と、その内側で実行される検索・保存処理を確認します。そのうえで、まとめて取得できないか、一括更新やupsertを使えるか、LaravelのchunkById()などで分割処理できるかを検討します。まとめて処理する方法にも、ユニークキーや一括処理時のメモリ量などの前提があるため、置き換え後の挙動も確認します。
EloquentのN+1を確認する
Eloquentでは、Modelを取得した後にリレーションへアクセスすると、その時点で関連データを取得することがあります。たとえば、部署を持つ従業員を一覧で表示するコードを考えます。
$employees = Employee::get();
foreach ($employees as $employee) {
echo $employee->department->name;
}
departmentが遅延ロードされる場合、最初に従業員を取得するSQLに加え、従業員ごとに部署を取得するSQLが発行される可能性があります。従業員の件数が増えるほどクエリ数も増える、このような状態をN+1問題と呼びます。
一覧で使うリレーションが分かっている場合は、with()によるEager Loadingを検討します。
$employees = Employee::with('department')->get();
foreach ($employees as $employee) {
echo $employee->department->name;
}
ただし、Eager Loadingを加えれば常に速くなるわけではありません。画面で使わないリレーションまで取得すると、取得データが増えます。実際に参照するリレーションと取得件数を確かめて選ぶ必要があります。
大量取得と件数の上限を確認する
all()やget()を見つけたら、その処理で最大何件取得するのかを確認します。クエリの時間だけでなく、DBからPHPへの転送量、Modelの生成、PHPのメモリ使用量、取得後の処理にも影響します。
画面表示ならページングが必要か、バッチなら一定件数ごとに処理できるかを確認します。大量データを扱う場合、LaravelのchunkById()やlazyById()が選択肢になります。処理中に対象行を更新する場合は、取得条件や並び順によって読み飛ばし・重複が起きないかも確認してください。
ページングでは、深いページを取得するLIMIT ... OFFSET ...が候補になります。ページが進むほど無視する行が増える場合があるため、MySQLで実際の実行計画を確認します。カーソル方式が使えるかは、並び順や画面要件も含めて判断します。
検索条件とインデックスを確認する
WHERE句やJOINの条件に使うカラムにインデックスがあるかは、コードだけでなくMigrationや実際のDBスキーマも確認します。検索条件が複数ある場合は、それぞれのカラムにインデックスがあれば十分とは限りません。条件の組み合わせ、並び順、データの偏りによって適切なインデックスは変わります。
たとえば、次のような条件で検索する処理があるとします。
$orders = Order::query()
->where('status', 'pending')
->where('created_at', '>=', $from)
->orderBy('created_at')
->get();
status、created_atのインデックスや複合インデックスを検討できますが、どれが有効かはデータ量や値の分布にもよります。インデックスを増やすと検索が改善する場合がある一方、INSERTやUPDATEの負荷、ディスク使用量も増えます。レビューの段階で追加を決めつけず、実行計画で確認します。
次のような条件も調査候補になります。
- WHERE句でカラムに関数を適用している。
LIKE '%keyword%'のような前方ワイルドカード検索をしている。- 大量のIDを
whereIn()へ渡している。 - JOIN、ORDER BY、GROUP BYを大量データに対して行っている。
これらは必ず遅いという意味ではありません。MySQLのバージョン、照合順序、インデックス、条件に一致する件数などを確認するための手掛かりです。
Laravelで発行SQLと実行時間を確認する
コードから候補を見つけたら、Laravelが実際に発行したSQLと実行時間を確認します。Laravel 13.xでは、DB::listen()でクエリ実行イベントを受け取れます。たとえば調査中だけAppServiceProviderへ次のようなログを追加できます。
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
public function boot(): void
{
DB::listen(function (QueryExecuted $query): void {
if ($query->time >= 100) {
logger()->warning('Slow SQL candidate', [
'sql' => $query->sql,
'time_ms' => $query->time,
]);
}
});
}
この例では100ミリ秒を調査用の仮のしきい値にしています。アプリケーション共通の性能基準を示す値ではないため、環境や処理内容に合わせて調整してください。また、このイベントの時間は個々のクエリに対する値です。リクエスト全体でDBクエリに使った時間を監視したい場合は、LaravelのDB::whenQueryingForLongerThan()も使えます。
SQLのバインド値には、メールアドレスや個人情報などが含まれることがあります。bindingsや、値を埋め込んだSQLをそのままログへ出すと情報が残るため、共有・保存先、マスキング、保存期間に注意します。上の例ではSQL文と時間だけを記録していますが、SQL文にもテーブル名や内部構造が含まれる可能性があります。
MySQLのEXPLAINと実測で候補を確かめる
発行SQLを確認したら、Migrationの定義だけでなく、対象環境にあるテーブル件数やインデックスも確認します。そのうえで、MySQLのEXPLAINを使って実行計画を見ます。出力には、オプティマイザーが選択したインデックスや、読み取ると見積もった行数などが含まれます。
EXPLAIN
SELECT id, employee_code, name
FROM employees
WHERE employee_code = 'E1001';
EXPLAINの推定だけでなく、実際の処理時間や読み取った行数を確認したい場合は、MySQL 8.0.18以降のEXPLAIN ANALYZEを使えます。このコマンドは実行計画を表示するだけでなく、対象のクエリを実行します。SELECT文でも大きなテーブルを走査すればDBへ負荷がかかるため、本番環境で不用意に実行せず、対象データと実行環境を確認してください。
最終的には、実データに近い件数や分布の環境で、同じ条件のもと変更前後を計測します。開発用の少量データだけでは、本番相当の処理時間や実行計画を再現できないことがあります。
レビュー指摘は次の調査につながる形にする
コードレビューでは、根拠がそろう前に「このSQLは遅い」と断定するより、確認したい理由と次の調査を示すと対応しやすくなります。
- 対象箇所:ループ内で社員コードごとにSELECTとUPDATEを実行している。
- 想定される影響:入力件数に応じてDBへのアクセス回数が増える。
- 確認したい情報:最大入力件数、社員コードのインデックス、実際のSQL回数と実行時間。
- 次の調査:対象データに近い環境でEXPLAINと処理全体の計測を行う。
SQLの実行時間が長く見える場合でも、クエリ自体ではなく、トランザクションやロックの待ち時間が影響していることがあります。SQLの実行計画と合わせて、長いトランザクションやループ内の外部通信なども確認します。
コードレビューは性能問題を早く見つける入口です。候補を絞った後にSQL、スキーマ、インデックス、実行計画、実測値を照らし合わせることで、必要な改善を判断できます。
参考資料
- Laravel 13.x Database: Listening for Query Events
- Laravel 13.x Database: Monitoring Cumulative Query Time
- Laravel 13.x Eloquent Relationships: Eager Loading
- Laravel 13.x Eloquent: Chunking Results
- MySQL 8.4 Reference Manual: EXPLAIN Statement
- MySQL 8.0 Reference Manual: EXPLAIN Statement
- MySQL 8.4 Reference Manual: EXPLAIN Output Format
- Laravel 13でSQLiteを使い、ModelとMigrationでユーザー一覧のCRUDを実装する
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
Windows 11でDocker Composeを使ってLaravel環境を作る:Container・Volume・Networkの基本
2026.10.01
Windows 11のDocker Desktop上に、DockerfileとDocker Composeを使ってLaravel・PHP・MySQL・Mailpitの開発環境を構築します。ImageとContainerの関係、Bind MountとNamed Volumeによるデータの保存、ポート公開とContainer間通信の違いを実例で確認。LaravelからMySQLへ接続するときに、localhostではなくService名を使う理由も説明します。
LaravelのArtisanコマンドを本番運用する:冪等性・二重実行防止・再実行の設計
2026.09.22
LaravelのArtisanコマンドでデータベースを更新する処理を、本番環境で安全に運用するための考え方を整理します。商品在庫の一括更新を例に、冪等性、dry-run、チャンク処理、トランザクション、二重実行防止、失敗後の再実行、ログや終了コード、実行前後の確認手順を紹介します。
Laravel 13のNamed Rate Limiterでレート制限を実装する:IP・ユーザー・入力値ごとの設定
2026.09.19
LaravelのNamed Rate Limiterを使い、無制限に繰り返されたくない処理へレート制限を追加する方法を解説します。RateLimiter::for()とthrottleミドルウェアの基本から、IPアドレス・ユーザーID・入力値による制限、複数の制限、429レスポンス、ログイン処理への応用、Redis利用時の注意点まで整理します。
Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで
2026.09.18
Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。