ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い

  • 975
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

ActiveRecordでN+1クエリを潰すためにeager loadingを行う場合、preloadincludeseager_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に対してjoinsreferencesも呼んでいる
  • 任意の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になる。

ちなみに、referencesincludesのスイッチング専用メソッドである

まとめ

メソッド キャッシュ クエリ 用途
joins しない 単数 絞り込み
eager_load する 単数 キャッシュと絞り込み
preload する 複数 キャッシュ
includes する 場合による キャッシュ、必要なら絞り込み

そのテーブルとのJOINを禁止したいケースではpreloadを指定し、JOINしても問題なくてとりあえずeager loadingしたい場合はincludesを使い、必ずJOINしたい場合はeager_loadを使いましょう。