Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした