はじめに
joins/preload/eager_loadについて調べた内容を整理します。
メソッドの比較
メソッド | JOIN | 子の条件で親を絞る | N+1防止 | 発行されるSQL | 用途 |
---|---|---|---|---|---|
joins | INNER JOIN | できる | × | 1クエリ | 条件付き検索 |
preload | しない | できない | 〇 | 2クエリ | 関連のみ使用 |
eager_load | LEFT OUTER JOIN | できる | 〇 | 1クエリ | 条件付き検索+ N+1防止 |
JOINの種類
種類 | 残るレコード | マッチしない右側の扱い |
---|---|---|
INNER JOIN | 両方にマッチする行のみ | 除外される |
LEFT OUTER JOIN | 左側の全行 | NULLで埋められる |
joins
User.joins(:books).where(books: { title: "Ruby" })
発行されるSQL
SELECT "users".* FROM "users"
INNER JOIN "books" ON "books"."user_id" = "users"."id"
WHERE "books"."title" = 'Ruby'
- 子テーブルbooksの条件で親テーブルusersのレコードを絞り込める
- 関連するbooksは読み込まれていないため、user.booksを呼ぶとN+1問題が発生する
-
eager_load
のようにLEFT OUTER JOIN
で全カラムを取得しないため、結果が軽く、重複行が発生しない
preload
User.preload(:books)
発行されるSQL
-- 親レコード(users)を取得
SELECT "users".* FROM "users";
-- 子レコード(books)を取得
SELECT "books".* FROM "books" WHERE "books"."user_id" IN (1, 2, 3, ...);
- SQLが親テーブルと子テーブルの2回発行される
- Railsが取得したusersとbooksをメモリ上で照合して
user.books
を使えるようにする(N+1防止)- user.id == book.user_id を照合
- 関連するBookをUserに紐づける
- JOINをしないため、SQLが軽い
- 関連は使えるが、条件は効かない
条件は使えない
User.preload(:books).where(books: { title: "Ruby入門" }) # 実行できない
User.preload(:books).order('books.title ASC') # 実行できない
関連は使える:user.booksはすでに読み込まれている
users.each do |user|
user.books.each do |book|
puts "#{user.name} - #{book.title}"
end
end
eager_load
User.eager_load(:books).where(books: { title: "Ruby" })
発行されるSQL
SELECT "users"."id", ..., "books"."id", ..., "books"."title"
FROM "users"
LEFT OUTER JOIN "books" ON "books"."user_id" = "users"."id"
WHERE "books"."title" = 'Ruby'
- LEFT OUTER JOINを使って、親と子を1クエリで取得する
- 関連の条件やソートがSQLに反映される(N+1防止)
- has_manyの関連の場合は親が重複する
includes
preload
とeager_load
を自動で使い分ける
- 判断基準
- 関連に条件やソートがない ->
preload
- 関連のカラムにwhere や orderを使っている ->
eager_load
- 関連に条件やソートがない ->