ActiveRecord N+1 (joins, preload, include, eager_load)

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

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

1. 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 joinしたい場合 let_joinsを使う(Rails5)
  • associationをキャッシュしない
    • つまりクエリは遅い
    • しかし、メモリの節約になる(結果セットが遅いから)

使うケース

  • JOINして条件を絞り込みたいが、JOINするテーブルのデータを使わない場合 はjoinsを使うのが良い

left joins

# rails 4.x
authors = Author.join('LEFT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"')
                .uniq
                .select("authors.*, COUNT(posts.*) as posts_count")
                .group("authors.id")
# rails 5
authors = Author.left_outer_joins(:posts)
                .uniq
                .select("authors.*, COUNT(posts.*) as posts_count")
                .group("authors.id")

# 複数のleft join
Author.left_joins :posts, :comments

2. 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したテーブルで絞込ができる

3. 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したテーブルによって絞り込もうとすると、例外を投げる。

4. 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に対してjoinsかreferencesも呼んでいる
  • 任意のassociationに対してeager_loadも呼んでいる

のうちいずれかを満たす場合、eager_loadと同じ挙動(LEFT JOIN)を行い、 そうでなければpreloadと同じ挙動(クエリを分けて実行)をする。 絞り込みが必要な時に例外を投げずeager_loadにfallbackするpreload。

References

results matching ""

    No results matching ""