0
0

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で理解する N+1問題

0
Posted at

Laravelで理解する N+1問題

〜ORMの便利さと遅延ロードの正体〜

Laravel(Eloquent)を使っていると、
N+1問題 という言葉をよく耳にします。

N+1問題とは、
本来まとめて取得できるはずのデータを、必要になるたびに個別取得してしまうことで、
結果的に「1 + N 回」のクエリが発行されてしまう問題
です。


引越しを例にイメージを掴む

Eager Loading(先に全部取る)

引っ越し当日に
「使うかもしれない物を全部トラックに積む」

メリット

  • 運ぶ回数は1回
  • 必要な物はすぐ使える

デメリット

  • トラックが重くなる(引越し料金が高くなる)
  • 引越し先に収納スペースが必要
  • 結果的に不要なものも運ぶ

Lazy Loading(後で取る)

引っ越し後に
「必要になった物だけ、実家に取りに行く」

メリット

  • 無駄な荷物を運ばない
  • トラックが軽くなる(引越し料金が安くなる)
  • 引越し先に収納スペースが少なくて済む

デメリット

  • 必要になったときに取りに行く必要がある
  • 何度も行くと大変

この記事では、

  • N+1問題とは何か(根本)
  • Post / comments のテーブル構造とEloquentの関係
  • なぜ原因が ORM の遅延ロードなのか
  • なぜ ORM を使うと N+1 が起きやすいのか
  • Laravelでの正しい対策
  • SQL直書き系(MyBatis等)との違い

を整理します。


前提:Post と comments のテーブル構造

この記事では、以下のような「1対多」の関係を前提にします。

  • 1つの Post に複数の Comment
  • Comment は post_id で Post に紐づく

posts テーブル

CREATE TABLE posts (
  id BIGINT PRIMARY KEY,
  title VARCHAR(255)
);

comments テーブル

CREATE TABLE comments (
  id BIGINT PRIMARY KEY,
  post_id BIGINT,
  body TEXT
);

Eloquentのリレーション定義

class Post extends Model
{
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Comment extends Model
{
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

N+1問題とは何か(根本)

N+1問題とは、

本来はまとめて取得できる関連データを、
ループの中で1件ずつ取得してしまい、
クエリ数が「1 + N」になってしまう問題

です。

重要なのは、

「クエリが多い」ことではなく、
「ORMの挙動によって 1 + N という構造になること」

です。


なぜ「N+1」と呼ばれるのか

$posts = Post::all(); // 親を取得
foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        // コメント処理
    }
}

発行されるSQL

SELECT * FROM posts;

SELECT * FROM comments WHERE post_id = 1;
SELECT * FROM comments WHERE post_id = 2;
SELECT * FROM comments WHERE post_id = 3;
...
  • 親取得:1回
  • 子取得:N回

👉 1 + N = N+1


なぜ原因は「ORMの遅延ロード」なのか

Eloquentのリレーションは、デフォルトで 遅延ロード(Lazy Loading) です。

$posts = Post::all();

この時点では、

  • posts テーブルのみ取得
  • comments は まだ取得していない

リレーションに初めてアクセスした瞬間

foreach ($posts as $post) {
    $post->comments;
}

ORMは内部で次の判断をします。

  1. 親はすでに取得済み
  2. この Post の comments は未取得
  3. 今必要になった
  4. その場でSQLを発行

これが N 回繰り返され、N+1になります。


N+1が発生する「本当の根本理由」

N+1問題は、ORMが便利に作られていることの副作用です。

ORMはなぜ便利か

  • オブジェクトとして扱える
  • SQLを書かなくても動く
  • リレーションをプロパティのように扱える
$post->comments;

この1行で、

  • SQLが発行される
  • DBにアクセスしている

ことを 意識しなくても動く


その結果どうなるか

  • 開発者が「どのタイミングでSQLが発行されるか」を意識しない
  • ループ内で何気なくリレーションに触る
  • 気づいたらN+1

👉 N+1は「知識不足」ではなく「抽象化の副作用」


Laravelでの正しい対策(Eager Loading)

$posts = Post::with('comments')->get();
SELECT * FROM posts;
SELECT * FROM comments WHERE post_id IN (...);

👉 2クエリで完了


SQL直書き系(MyBatis等)ではどうか?

SQL直書きの場合

SELECT p.*, c.*
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id;
  • 実行されるSQLが明示的
  • JOINしないと関連が取れないことが可視化されている

👉 同じ構造のN+1は起きにくい


遅延ロードが本当に適切な典型例

条件次第で「後から」必要になる

$posts = Post::all();

foreach ($posts as $post) {
    if ($post->is_commentable) {
        foreach ($post->comments as $comment) {
            // 初めてここで取得
        }
    }
}
  • 最初は取得しない
  • 条件成立時のみ取得
  • Lazy Loading が意味を持つ

詳細ページ(Nが小さい)

$post = Post::findOrFail($id);
$comments = $post->comments;
  • 最大でも2クエリ
  • N+1の N が大きくなりにくい

N+1とは少し異なるが、同じような設計上のミス

  • forでIDを回してSELECT
  • DAOをループで呼ぶ

といった実装でも、

  • N回クエリ
  • パフォーマンス劣化

は普通に起きます。

ただしこれは ORMのN+1とは性質が違う ものです。

観点 ORMのN+1 SQL直書き
原因 遅延ロード 実装ミス
見えにくさ 高い 低い
起きやすさ 高い 低い

クエリパラメータ100件をforで回すケース

クエリパラメータで100件のIDが渡ってきて、
それをforで回して100回SELECTしているコード

foreach ($ids as $id) {
    User::where('id', $id)->first();
}
  • これはN+1ではない
  • 単なるループクエリ
  • しかし危険

複数(それも大量)のIDを取得する可能性があるなら、

User::whereIn('id', $ids)->get();

とすべきです。


まとめ

  • N+1問題の正体は ORMの便利さの副作用
  • 遅延ロード自体は悪ではない
  • 「どのタイミングでSQLが発行されるか」を意識することが重要
  • SQL直書きでは処理が表面化しやすく、N+1は起きにくい
  • Laravelでは
    • 必ず使う → with()
    • 後で使うかも → Lazy Loading
    • Nが大きい → 要注意

N+1問題を理解するとは、
ORMの裏で何が起きているかを想像できるようになることです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?