💎

ActiveRecord:preload、Includes、eager_load、joinsの挙動の違い

2021/12/12に公開2

ActiveRecordでN+1クエリを解消するためにpreloadやincludesやeager_loadを使うが、それらの違いについてあまり理解できていなかったのでまとめてみました。

それぞれ、大きく違いがあるのは下記3点かなと思っています。

  • 結合でデータを取得するか、クエリを2つ発行して取得するか?
  • 結合先のテーブルで絞り込むことはできるか?
  • 結合先のテーブルの情報を取得することができるか?

以下、詳しく書いていきます!

preload

  • 結合でデータを取得するか、クエリを2つ発行して取得するか?
    →クエリを2つ発行して取得する
  • 結合先のテーブルで絞り込むことはできるか?
    →できない
  • 結合先のテーブルの情報を取得することができるか?
    →できない

発行されるSQL

User.preload(:posts).to_a

SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (1)

preloadeは常に2つのSQLを生成するために、where条件でpostsテーブルを使用することができない。次のクエリはエラーとなる

結合先のテーブルで絞ろうとすると例外となる

User.preload(:posts).where("posts.desc='ruby is awesome'")

例外

主のテーブルで絞ることはできる

User.preload(:posts).where("users.name='Neeraj'")

SELECT "users".* FROM "users"  WHERE (users.name='Neeraj')
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (3)

eager_load

  • 結合でデータを取得するか、クエリを2つ発行して取得するか?
    →外部結合でデータを取得する
  • 結合先のテーブルで絞り込むことはできるか?
    →できる
  • 結合先のテーブルの情報を取得することができるか?
    →できる

発行されるSQL

User.eager_load(:posts).to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1, "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"

LEFT OUTER JOINを使用して1つのクエリですべての関連付けが読み込まれます。

joins

  • 結合でデータを取得するか、クエリを2つ発行して取得するか?
    →内部結合でデータを取得する
  • 結合先のテーブルで絞り込むことはできるか?
    →できる
  • 結合先のテーブルの情報を取得することができるか?
    →できない

発行されるSQL

User.joins(:posts)

SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"

Joinsは、内部結合を使用して関連付けデータを取得する。
しかし、結合先の情報は取得しない点が他と違う。
なので、JOINして条件を絞り込みたいが、JOINするテーブルのデータを使わない場合は、joinsを使うのが良い。(メモリの消費が少ない)

注意点は結合先のデータを取得する際に、全く同じデータを取得してしまう。(1対多のデータだと)

テストデータ

u = User.create name: 'Neeraj'
u.posts.create! title: 'ruby', desc: 'ruby is awesome'
u.posts.create! title: 'rails', desc: 'rails is awesome'
u.posts.create! title: 'JavaScript', desc: 'JavaScript is awesome'

u = User.create name: 'Neil'
u.posts.create! title: 'JavaScript', desc: 'Javascript is awesome'

u = User.create name: 'Trisha'
<User id: 9, name: "Neeraj">
<User id: 9, name: "Neeraj">#データが被る
<User id: 9, name: "Neeraj">#データが被る
<User id: 10, name: "Neil">

こういった場合は、重複のデータを省くようにdistinctにする

User.joins(:posts).select('distinct users.*').to_a

includes

結合先のテーブルを絞り込もうかするかどうかで挙動を変える(ややこしい、、。)

結合先のテーブルで絞り込まない場合はpreloadと同じ挙動
結合先のテーブルで絞り込まない場合はeager_loadと同じ挙動

結合先のテーブルで絞り込まない場合はpreloadを同じ挙動

User.includes(:posts).to_a

SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (1)

複数のクエリでデータを取得する

結合先のテーブルで絞り込まない場合はpreloadを同じ挙動

User.preload(:posts)where( "posts.desc = 'ruby is awesome'").to_a

SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1,
       "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
WHERE (posts.desc = "ruby is awesome")

外部結合でデータを取得する。ここがpreloadと違う点。prelaodだと例外が発生する。

まとめ

テーブルのJOINを禁止したい場合はpreloadを使う。
必ずJOINしたい場合はeager_loadを使う。
JOINしても問題ない場合は、includesを使う。
joinsは結合先のデータを使わなくてもいい場合、メモリ消費を抑えたい場合に使う。

間違いがあればご指摘ください!

参考にした記事

Preload, Eagerload, Includes and Joins
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita

Discussion