🙄

N+1問題の初歩編

2024/04/02に公開

はじめに

OO小学校>1年生>1組>佐藤太郎>消しゴム
というようなクラスが連なってるとします。
子には親のIDが必ず入っています。
世界一覧
ID1:日本
ID2:アメリカ
日本一覧
ID1:北海道
ID2:東京都



という感じです。

この時、消しゴムの数を調べたい時、どうしますか?
消しゴムテーブルをカウントするだけですね。

$count = 消しゴム::count();

では、
「ID1の人が持っている消しゴムの数を数える」、とするとどうしますか?








$count = 消しゴム::where(氏名ID,1)->count();

となりますね
ID2なら

$count = 消しゴム::where(氏名ID,2)->count();

となります。

ID1~100の人の合計を取得するときにどうなりますか?

上記の結果を足し算しますか?


$id1_count = 消しゴム::where(氏名ID,1)->count();
$id2_count = 消しゴム::where(氏名ID,2)->count();
・
・
・

$sum = $id1_count + $id2_count + ・・・;

流石にこんなことをしないですよね?
では下記のようにしたらどうでしょう?

$sum = getCountByBetWeenId($start, $end);

function getCountByBetWeenId ($start, $end)
{
    $count = 0;
    for ($i = $start; $i++; $i > $end) {
        $count += 消しゴム::where(氏名ID,$i)->count();
    }
    return $count;
}

コードは短く済みそうです、、、
が人数分のクエリが発行されてしまいます。
と言ってもSQLはすごく優秀で、100回くらいでしたら、100msくらいで取得できちゃいます。

ですが、これが10万とするとどうなるでしょうか?
1回でも10回でも、1回あたりのスピードはほとんど変わらないので、ここがボトルネックになります。
つまり、10万ms = 100秒となります。
100秒待たされるシステム、どうでしょうか?一般的にシステムは3秒以内にレスポンスするのがルール、WEBサイトなどは1秒以内がルールとなっています。

しかし、これは通常下記のように取得しましょう!

$count = 消しゴム::whereBetween(氏名ID, [1,100])->count();

連続していない範囲でもこのように取得できます

$ids = [1,3,5,7,9];
$count = 消しゴム::whereIn(氏名ID, $ids)->count();

次に問題なるのは、
1組に所属する児童の消しゴムの数を数える時です。
1組に所属する児童は下記のように取れます。

$1年生 = 1年生::where(ID, 1)->get();

1組に所属する児童の消しゴムの数の総数はこうなります。

$1年生 = 1年生::where(ID, 1)->get();
$sum = 0;
foreach($1年生 as $児童) {
    $sum += 消しゴム::where(氏名ID,$児童->id)->count()
}

これもまた、児童分データベースにアクセスがあります。

こんな感じで回避できます。
クエリを発行せず、idの一覧のみを作成します。(pluckで代用可能)

$1年生 = 1年生::where(ID, 1)->get();
$1年生ids = [];
foreach($1年生 as $児童) {
    $1年生ids[] = $児童->id;
}
$count = 消しゴム::whereIn(氏名ID, $1年生ids)->count();

学校全体も下記です

$芝浦小学校現役の組ID一覧 = $芝浦小学校::whereIn(学年, [1,6])->pluck(ID);
$組一覧 =::whereIn(ID, $芝浦小学校現役の組ID一覧)->get();
$児童ids = [];
foreach($組一覧 as $児童) {
    $児童ids[] = $児童->id;
}
$count = 消しゴム::whereIn(氏名ID, $児童ids)->count();

Discussion