N+1問題の初歩編
はじめに
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