Rails

INNER JOIN で eager loading

More than 3 years have passed since last update.

INNER JOIN じゃないと不味いが joins だと N + 1 問題が発生してしまう場合にどうするかを調べた。

結論から言うと joins + preload または joins + eager_load を使うと INNER JOIN を使って結合した上で関連オブジェクトも即時フェッチしてくれる。

試したのは Rails 4.1.6。


joins + preload


  • INNER JOIN で結合

  • クエリ複数回(指定した関連オブジェクトの分)

  • 即時フェッチ

> Post.joins(:comments).preload(:comments)

Post Load (0.3ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31)


joins + eager_load


  • INNER JOIN で結合

  • クエリ一発

  • 即時フェッチ

> Post.joins(:comments).eager_load(:comments)

SQL (0.4ms) SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."lock_version" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "comments"."id" AS t1_r0, "comments"."post_id" AS t1_r1, "comments"."name" AS t1_r2, "comments"."body" AS t1_r3, "comments"."lock_version" AS t1_r4, "comments"."created_at" AS t1_r5, "comments"."updated_at" AS t1_r6 FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"

ちなみに他のメソッドだと以下のようになる。


joins


  • INNER JOIN で結合

  • 遅延フェッチ

> Post.joins(:comments)

Post Load (0.3ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"


includes


  • JOIN 使わない

  • クエリ複数回(指定した関連オブジェクトの分)

  • 即時フェッチ

> Post.includes(:comments)

Post Load (0.3ms) SELECT "posts".* FROM "posts"
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31)


includes + references


  • LEFT OUTER JOIN で結合

  • クエリ一発

  • 即時フェッチ

> Post.includes(:comments).references(:comments)

SQL (0.4ms) SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."lock_version" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "comments"."id" AS t1_r0, "comments"."post_id" AS t1_r1, "comments"."name" AS t1_r2, "comments"."body" AS t1_r3, "comments"."lock_version" AS t1_r4, "comments"."created_at" AS t1_r5, "comments"."updated_at" AS t1_r6 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"


eager_load


  • includes + references と同じ

> Post.eager_load(:comments)

SQL (0.4ms) SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."lock_version" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "comments"."id" AS t1_r0, "comments"."post_id" AS t1_r1, "comments"."name" AS t1_r2, "comments"."body" AS t1_r3, "comments"."lock_version" AS t1_r4, "comments"."created_at" AS t1_r5, "comments"."updated_at" AS t1_r6 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"


preload


  • includes と同じ

> Post.preload(:comments)

Post Load (0.3ms) SELECT "posts".* FROM "posts"
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31)


参考

3 ways to do eager loading (preloading) in Rails 3 & 4 - Arkency Blog

http://blog.arkency.com/2013/12/rails4-preloading/

Preload, Eagerload, Includes and Joins · BigBinary Blog

http://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html

Rails - ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita

http://qiita.com/k0kubun/items/80c5a5494f53bb88dc58