Edited at

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

More than 3 years have passed since last update.

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を使いましょう。