ActiveRecordでN+1クエリを潰すためにeager loadingを行う場合、preload
やincludes
やeager_load
が役に立つ。
Preload, Eagerload, Includes and Joinsという記事にそれらの違いがよくまとめられているんだけど、includesが挙動を変える条件があまり正確に書かれていなくて自信が持てなかったし、そもそも記事が古いのでRails4.1.5のソースを読んで調べた。
せっかく調べたので、全体を通して日本語でまとめてみようと思う。
joins
User.joins(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
デフォルトでINNER JOINを行う。LEFT OUTER JOINを行いたい時はleft_joins
を使う。
他の3つとの違いは、associationをキャッシュしないこと。
associationをキャッシュしないのでeager loadingには使えないが、ActiveRecordのオブジェクトをキャッシュしない分メモリの消費を抑えられる。
なので、JOINして条件を絞り込みたいが、JOINするテーブルのデータを使わない場合はjoins
を使うのが良い。
User.joins(*).where(posts: {*})
とかUser.joins(*).merge(Post.*)
みたいに使う。
eager_load
User.eager_load(:posts)
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`
User.eager_load(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
指定したassociationをLEFT OUTER JOINで引いてキャッシュする。
クエリの数が1個で済むので場合によってはpreload
より速い。
JOINしているので、preload
と違って、joins
と同じようにJOINしたテーブルで絞込ができる。
preload
User.preload(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)
User.preload(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users` WHERE `posts`.`id` = 1
# => Mysql2::Error: Unknown column 'posts.id' in 'where clause': SELECT `users`.* FROM `users` WHERE `posts`.`id` = 1
指定したassociationを複数のクエリに分けて引いてキャッシュする。
複数のassociationをeager loadingするときとか、あまりJOINしたくないでかいテーブルを扱うときはpreloadを使うのがよさそう。
preloadしたテーブルによって絞り込もうとすると、例外を投げる。
includes
User.includes(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)
User.includes(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
-
includes
したテーブルでwhere
による絞り込みを行っている -
includes
したassociationに対してjoins
かreferences
も呼んでいる - 任意のassociationに対して
eager_load
も呼んでいる
のうちいずれかを満たす場合、eager_load
と同じ挙動(LEFT JOIN)を行い、
そうでなければpreload
と同じ挙動(クエリを分けて実行)をする。
絞り込みが必要な時に例外を投げずeager_load
にfallbackするpreload
。
根拠
def eager_loading?
@should_eager_load ||=
eager_load_values.any? ||
includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end
ActiveRecord::Relation#eager_loading?がtrueの場合LEFT JOINを行い、falseの場合クエリを分けて読み込む。
eager_load_values
とかincludes_values
はeager_loadしたやつとincludesしたときに追加されるやつで、そのまま。
joined_includes_valuesはincludesしたやつをわざわざjoinsもしてる奴のリスト。
references_eager_loaded_tables?はJOIN元以外にreferences
が行われている場合にtrueになる。なお、whereで別テーブルによる絞り込みを行った場合もreferencesが行われるため、この場合もtrueになる。
ちなみに、references
はincludes
のスイッチング専用メソッドである。
まとめ
メソッド | キャッシュ | クエリ | 用途 |
---|---|---|---|
joins | しない | 単数 | 絞り込み |
eager_load | する | 単数 | キャッシュと絞り込み |
preload | する | 複数 | キャッシュ |
includes | する | 場合による | キャッシュ、必要なら絞り込み |
そのテーブルとのJOINを禁止したいケースではpreload
を指定し、JOINしても問題なくてとりあえずeager loadingしたい場合はincludes
を使い、必ずJOINしたい場合はeager_load
を使いましょう。