【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【WordPress 専用】クラウドサーバー『ウェブスピード』

【WordPress 専用】クラウドサーバー『ウェブスピード』

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【中国現地企業に対応】中国クラウド / サーバー構築・運用保守

【中国現地企業に対応】中国クラウド / サーバー構築・運用保守

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

【EagerLoad】leftJoin と with はどちらが有用なのか【N+1問題】

「生のクエリを書いたことない奴はガチで危機感持ったほうがいい」
と、最近よく先輩や上長から言われているシステム開発部の榎木です。
(ちなみに新卒研修のMySQL研修担当になったので、ガチで危機感を感じています)

N+1問題

(私の話は置いといて)
このようなリレーションが組まれていたとします。

class Bolg
{
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

Bolg モデルを取得して

$blogs= Bolg::query()->first();

ここで下記のクエリが発行されます

"select * from `blogs`"

さらにループで回せば、各Blogモデルに紐付く Comment モデルを全部取得できます。

$comments= $blogs->map(function ($blog) {
    return $blogs->comments->first();
});

当然ながらループしているので、Chapter の数ぶんだけ、下記クエリが叩かれることになります。

"select * from `blogs` where `blogs`.`blog_id` = ? and `blogs`.`blog_id` is not null"

最初の全件取得のクエリの1回 + N回のクエリが発行されるんですね。
これが1+N もといN+1問題です。

何が問題なのかというと、当然ながら Chapter の数ぶんだけクエリが発行されるので、負荷が掛かってかかってシンプルに問題というわけですね。

N+1 を回避してくれる join と with

本題の入り口まで来ました。

前述のように、都度都度クエリを発行するのはめちゃくちゃ効率が悪いので、(left)joinかwithを使います。
(ちなみに場合によっては都度取得した方がいいこともあるのでよく選んでください)
これらはそれぞれ大量のクエリが流れないようにするために使うことができます。
ただ、それぞれやっていることが違います。

join って?

join は、内部結合(Inner Join)を行っています。
テーブル間で共通するデータを取得し、それ以外を排除します。

leftJoin は左外部結合(Left Outer Join)を実行します。
左側のテーブルのすべての行を返し、右側のテーブルに一致する行があればそれも返します。
左側のテーブルの全データを保持しつつ、結合条件に一致するデータを取得します。

これらの join が何なのかというと、SQL の機能です。
クエリ内で複数のテーブルを結合しているんですね。
クエリ的にはこんな感じです。

0 => array:3[
    "query" => "select * from `blogs` inner join `comments` on `blogs`.`id` = `blog_id`"
    "bindings" => []
    "time" => 1.5
],

なんとクエリは1回です。
すさまじい進歩を感じますね。

じゃあ with は?

with

with のクエリは2回です。
join には負けますが、ループはN+1回も流れることを考えるととてつもない進歩です。

ちなみにwithの引数とする文字列ですが、これはモデルファイルの中のリレーション定義メソッドの名前です。
(実質リレーション先のモデル名とイコールですが、メソッドとして定義してないと、with を使えないので気を付けてください)

$blogAndComments= Blog::query()
    ->with('comments')
    ->first();

クエリを見ると、whereIn しているおかげで一括での取得ができていますね。

0 => array:3[
    "query" => "select * from `blogs`"
    "bindings" => []
    "time" => 0.5
],

1 => array:3[
    "query" => "select * from `blogs` where `blogs`.`blogt_id` in (?)"
    "bindings" => []
    "time" => 1.5
],

それぞれの特徴と使い分け

2つの方法でモデルを取得したわけですが、それぞれの特徴を見ていきましょう。

● (left)Join
結合条件をもとにモデルを合体させます。
join とは SQL の機能であり、複数のテーブルのデータを一度に取得するために使います。
取得の際、結合条件に一致するぶんだけのデータが作られます
例で言うなら、Comment モデルと同じ数だけ最終的なデータが作られると考えてよいでしょう。
これが複数テーブルを結合対象にすると不正なデータができたりします。

ただ一括で取得できるのは魅力的ですし、モデルとして必要でない場合や、
シンプルでフラットなデータが欲しいときに最適といえます。

● with
モデルを合体させず連想配列の形で取得します。
ORM におけるモデルのリレーション関係を事前に読み込むことによって、N+1問題を解決してくれます。

例で言うと、Bolg モデルの下にそれに紐付く Comment モデルが、relations というプロパティの中に存在するようになります。
モデルが親子関係の階層順にネストして取得できるので、モデルを合体させません。
関連モデルや関連データを一度に取得し、冗長なデータを生みません。
また、取得データをそのままにモデルとして扱いたいときに便利です。

ちなみに with は、(ModelA.ModelB.ModelC.)といった具合にリレーションが続く限り取得できますが、ネストされるということは通過したモデルがすべて外郭として付いてきてしまうので、最終的には unset するなどして排除してあげる必要があります。

まとめ と 余談

情報が出そろったのでまとめて、あとは少し余談して終わりにします。

まとめ

いかがでしたでしょうか?
情報がたくさん出てきたので、表にしてみました。

どれが真に有用なのかはわかりませんね!!!
結果こそ雑に言うと複数テーブルのデータの取得なのですが、やっていることが違いすぎます。

なので、「欲しいデータ形に合わせて取得方法を変えてください」という、在り来たりなまとめです。
一番わかりやすいのは、モデルそのままの形なのか合体していて良いのか、というところが判別しやすい気がします。
あるいはネストとフラットのどちらの形のほうが今後の処理で使いやすいのか、という視点に立てば使い分けられそうです。

加えて言うなら取得したいデータの総量に応じて変えるべきなのかなと思っています。
with はリレーションを読み込むので、ネストが深くなればなるほど join よりも動作が重くなります。
つまり、、、場合によります。

余談(EagerLoad)

EagerLoad について書いておこうと思います。
ここまでで理解されているかもしれませんが要するに、都度 SQL を発行したくないよねということです。

これだけだと味気ないのでもう少し具体的な話に触れます。
「動的プロパティでないと EagerLoad が使えない」ということです。
(たぶん意味が分からないと思います)
Fateのフラガラックが分かればイメージしやすいです

まずは動的プロパティとリレーションメソッドの違いについてです。
つまりはこれと、

$blog= new Blog;
$blog->comments;

これの違いというわけです。

$blog= new Blog;
$blog->comments();

->commentsの場合は動的プロパティとなり、Collection が返ってきます。
また、アクセスされたときにだけリレーションのデータをロードする「遅延ロード」という性質があります。
対して ->comments() の場合はというと、リレーションオブジェクトになります。

動的プロパティではクエリビルダをつなげて書けませんが、
リレーションオブジェクトであれば ->comments()->where('create_user_id', 1)
みたいに繋げられます。

となるとリレーションオブジェクトとして取得したほうが良さそうに見えますよね。
ここで with をはじめとした EagerLoad たちの性質を思い出してください。

そう、事前にクエリを発行していますよね。
(relations プロパティの中に)データを取得してきているので、
$blog->comment->create_user_id と書いたときにクエリが走りません。

これが「動的プロパティでないと EagerLoad が使えない」
つまり、「EagerLoad するなら後の処理も動的プロパティにしないと意味なくなるよ!!」ということです。
逆説的にそうなる、といった具合です。
まさに「後より出て先に立つもの」ですね。

それじゃあ取得するときにどうやって条件・制約・検索かけるんだよ!!という方へ。

$blog= Bolg::with(['comments' => function ($query) {
    $query->where('create_user_id', 1);
}])->first();
$comments= $blog->comments;

こうすることで、EagerLoad と条件・制約・検索を兼ねることができるんですね。
ちなみによく使いそうな条件なら、モデルファイル内にメソッド化して置いておけば、可読性も高まりますし便利です。

おわり

この記事で少しでも世のデータベースへのアクセスが軽くなること祈っています。

この記事がお役に立てば【 いいね 】のご協力をお願いいたします!
4
読み込み中...
4 票, 平均: 1.00 / 14
84
X facebook はてなブックマーク pocket
【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

この記事をかいた人

About the author

えの木

FPS・RPG・MMO・クラフト系などなんでもやります。