IT-Q

with() を入れたら今度はメモリ使用量が増えて、別のところで落ちました。なぜですか。どうしますか。

Eager Loading は関連レコードを一度に全件メモリへ載せるためです。クエリ本数は減っても1件あたりのメモリは増えます。列を絞る、chunk で分ける、そもそも全件要るか見直します。

面接官が見ているのはEager Loading の代償を理解しているか。ここが本命。

これは「N+1 でした。どう直しますか。」への追撃質問です。

よくある答えと、面談でどう見られるか

  • 関連レコードを全件メモリに載せるから。select で列を絞り、chunk / lazy で分割する
    正解です。クエリ本数とメモリはトレードオフなので、どちらが効くかは件数次第、という理解まで言えると強いです。
  • with() がクエリを N 本発行し直しているから
    逆です。with() は本数を減らします。増えるのはメモリの方です。
  • Eager Loading が裏でキャッシュを持つから
    キャッシュは持ちません。取得した分をそのままメモリに保持しているだけです。
  • with() を使うと自動で JOIN になり、行数が掛け算になるから
    惜しいですが仕組みが違います。with() は JOIN ではなく whereIn の別クエリです。行の掛け算は起きません。増えるのは単純に保持する件数です。

面接官は何を見ているか

  • 「関連レコードを全件メモリに載せるから」と原因を言えた
  • select で列を絞る、という対処を挙げた
  • chunk / lazy による分割処理に触れた
  • cursor はリレーションを eager load できない、と知っている

模範解答

Eager Loading は関連レコードを一度に全件メモリへ載せます。クエリ本数は減りますが、1件あたりのメモリは増えるので、件数が多いと逆に苦しくなります。対処は3つ。(1) with('items:id,order_id,name') のように必要な列だけ取る、(2) chunk() や lazy() で分割して処理する、(3) そもそも一覧に全件必要か見直す。N+1 の解決とメモリはトレードオフなので、どちらが効くかは件数次第です。

列を絞るときの注意として、公式は「id と、関連する外部キーの列は必ず含めること」と明記しています。外部キーを外すと紐づけができず、リレーションが空になります。

分割処理では cursor() を挙げたくなりますが、ここでは選べません。cursor() はメモリに1件しか持たない仕組みなので、公式に「リレーションを eager load できない」と明記されています。with() と併用したいなら lazy() です。なお cursor() も PDO が結果を内部バッファに溜めるため、件数が極端に多いと結局メモリを使い切ります。