はじめに
Rubyを扱っていれば一度くらいはDB操作でjoins
やincludes
といったメソッドを使うことがあるかと思います。この2つのメソッドは意外と奥が深くてそれ故に理解するのが少しだけ難しい側面があります。久しぶりにjoins
とincludes
を使おうとしたのですが完全に使い方を忘れていて調べ直すはめになってしまいました。そこで今後繰り返し調べることがないように今回まとめておきたいと思います。
N+1問題について
まず用途についてですがN+1問題を回避する為に使うことが多いです。N+1問題とはループの中でDB操作を記述してしまい、結果クエリを大量に(ループ回数分)発行してしまうことです。
例えばAモデルとBモデルに関連があるとしてAテーブル全体に対してeach
で一つずつ処理をするとします。そして関連のあるBテーブルのレコードを検索してA・Bそれぞれの情報を出力する。これをAテーブルのレコード分繰り返します。
↑は典型的なN+1問題が起こるパターンです。クエリをAテーブルから全件取得する1回と、Aテーブルのレコード数分Bテーブルから関連のレコードを検索するので合計(N+1)回のクエリを無駄に発行しています。レコードが多いほどパフォーマンスが低下してしまいます。これを回避するのにjoins
やincludes
を使います。
joinsメソッドについて
まずはjoins
メソッドについて見てみます。仮にAとBに関連があるとしてA.joins(:B)
と実行したとします。この場合SQL的には次のようなクエリが実行されています。
※A、Bそれぞれのカラムは適当ですが関連を持つためにBがa_idを持っていると想定して下さい。
SELECT A.* FROM A INNER JOIN B ON B.a_id = A.id
所謂内部結合のためのINNER JOIN
が行われています。しかしここで気をつけなければいけないことが2点あります。1つ目は内部結合のための処理が行われているけれど返却値は内部結合ではないということです。
SELECT
の直後に注目してみるとA.*
となっており*
となっていません。つまりBのカラムは返却されていないということです。これは厳密には内部結合した状態からAのカラムのみを返却しているということです。SQLに慣れている人からすると逆に落とし穴にハマりやすいポイントなので注意して下さい。
2つ目は単純にSQLの結果が配列に格納されて返却されないということです。どういうことかと言うとjoins
は関連データが解決されたActiveRecord_Relationクラスの値が返却されるということです。レコードを取り出せばそれらはモデルインスタンスとしてActiveRecordクラスで定義されているメソッドを普通に使うことが出来ます。
2つ目の内容があるから結果的にjoins
は内部結合とイコールに見えますが、厳密には内部結合と同じ様なものと捉えた方が後々混乱することがないと思います。
includesのeager_loadとpreloadについて
includes
は場合によって自動でeager_load
を実行するかpreload
を実行するかよしなに振り分けるメソッドです。eager_load
とpreload
はどちらも関連を取得してキャッシュしてくれるものですが取得方法が異なります。まずはeager_load
とpreload
の単体での動作を確認します。
A.eager_load(:B)
と実行した場合はLEFT OUTER JOIN
が実行されます。つまり外部結合が行われます。こちらも返却値はActiveRecord_Relationクラスのオブジェクトなので注意が必要です。
続いてA.preload(:B)
と実行した場合は以下のSQLが実行される。
SELECT A.* FROM A
SELECT B.* FROM B WHERE B.a_id IN (1,2,...)
↑を見ると2回クリエが実行されているのが分かります。まずはAテーブルから全件を取得し、そのidとB.a_idが一致するBテーブルのレコードを取得しています。複数のクエリを使っている点は異なるけれどもアソシエーションを解決している点ではjoins
と変わりません。そのため大きいテーブルに対してはpreload
を使い、JOINするテーブルのデータを使わない場合はjoins
を使うというのが一般的な使い分けらしいです。
終わりに
正直未だに理解が不十分な箇所もあります。例えばincludes
がどうやってeager_load
とpreload
を振り分けているのかなど理解しきれませんでした。ですが取り敢えずそれぞれが何をしているのかを把握出来ただけでも今回は良しとしたいと思います。ORマッパーという巨人の肩に乗っかるのは楽でいいですね。