🧨

【Laravelで考える】N + 1問題

2024/11/07に公開

データベースのアクセス処理はWebアプリケーションのパフォーマンスに大きく影響します。今回は低パフォーマンスの原因となる「N+1問題」と Laravelにおける対策についてをまとめます。

N+1問題とは?

主にORM(Object-Relational Mapping)を使用する際に発生するパフォーマンス問題です。

具体的には、複数の関係するテーブルから値を取得する際、主クエリと各関連クエリが個別(N回)に実行されるため、データベースへの負荷が増大し、アプリケーションのレスポンスが低下する状態を指します。

ここでは、ユーザを管理するusersテーブル(親)とユーザが投稿した記事を管理するpostsテーブル(子)があるとします。

usersとpostsは1対多の関係です。

N+1の状態

class User extends App\Models\Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}
// ユーザを取得
$users = App\Models\User::all(); // ← usersテーブルの全レコードを取得

// 投稿を取得
foreach ($users as $user) {
    foreach ($user->posts as $post) {
        echo $post->content; // ← 該当の問題箇所
    }
}

親レコードを取得するためのクエリと、それに関連する子レコードを取得するためのクエリ(N個)が発行される状況です。

冒頭のall メソッドでは、usersテーブルのレコードしか取得していないため、postsテーブルの値を取得する場合は追加でクエリを発行する必要があります。

Lazy Loading ※遅延ロードとも呼びます。

どうすれば良いのか

Eager Loading 事前読み込み(読み方:「イーガー・ローディング 」)を使用します。

Lazy Loadingは親テーブルのレコードの都度クエリを実行していたのに対して、Eager Loadingを利用すると、関連するデータを一度に取得するためクエリ数を大幅に削減できます。

LaravelでEager Loadingを使用する場合はwith機能を使用します。

$users = App\Models\User::with('posts')->get(); // この時点で リレーションも含めてローディングする

// 各投稿のコメントを表示
foreach ($users as $user) {
    foreach ($user->posts as $post) {
        echo $post->content; // 不要なクリエを発行させない
    }
}

Eager Loadingの動き

$users = App\Models\User::with('posts')->get();
  1. メインモデルのデータ取得

    SELECT * FROM users;
    
  2. 関連テーブルのキーの取得
    取得したusersのレコードから、リレーションに使用する外部キーの値を取得します。usersテーブルのpostsテーブルと関連のあるカラムの値をリストアップします。
    [”user001”, ”user002”, ”user003”]

  3. postsの取得
    収集したキーを使用して、関連モデル(posts)のデータを一度のクエリで取得します。

    SELECT * FROM posts WHERE user_id IN ('user001', 'user002', 'user003');
    
  4. データのマッピング
    取得したpostsのレコードを、usersの各レコードに関連付けます。
    内部的には、userモデルのキーとpostsモデルの外部キーをマッチングしてデータをセットします。

Lazy Loadingの動き

// メインモデルのデータ取得
$users = App\Models\User::all();
  1. メインモデルのデータ取得

    SELECT * FROM users;
    
  2. postsの取得
    各ユーザーに対して個別のクエリが実行されます。例えば、ユーザーが3人いる場合、以下のようなクエリがそれぞれ発行されます。

    SELECT * FROM posts WHERE user_id = 'user001';
    SELECT * FROM posts WHERE user_id = 'user002';
    SELECT * FROM posts WHERE user_id = 'user003';
    

    これにより、N+1クエリ問題が発生します。具体的には、N個のユーザーに対してN個の追加クエリが実行されるため、全体でN+1個のクエリが実行されることになります。

  3. データのマッピング
    各クエリで取得した posts のレコードが、対応する user モデルに関連付けられます。これにより、$user->posts を通じて各ユーザーの投稿データにアクセスできるようになります。

Discussion