読者です 読者をやめる 読者になる 読者になる

Yamakichi’s blog

yamakichiの技術ブログ

Rails includes joins eager_load preload merge

INNER JOIN (内部結合)

結合する両方のテーブルどちらにも同じキーが存在するレコードのみを残し、それ以外は切り捨て。

OUTER JOIN (外部結合)

結合する両方のテーブルどちらにしか存在しないキーがあっても切り捨てずに取得する。 どちらのテーブルのレコードを取得するかで2通りの書き方が存在する。

  • LEFT OUTER JOIN (左外部結合)

    メインテーブルに存在するキーのレコードは、結合したいテーブルになくても表示する

  • RIGHT OUTER JOIN (右内部結合)

    結合したテーブルに存在するキーのレコードはメインテーブルになくても表示する

includes

includesはデータの先行読み込み。主に関連したmodelに対して参照を持ちたい場合に使う

N+1問題を解決する時などに用いる。

Topic.includes(:tags).each { |topic| p topic.tags.map(&:name) }
#=> SELECT "topics".* FROM "topics"
#=> SELECT "topic_tags".* FROM "topic_tags"  WHERE "topic_tags"."topic_id" IN (`取得したtopicsのID`)
#=> SELECT "tags".* FROM "tags"  WHERE "tags"."id" IN (`取得したtopic_tgasのid`)

これだと効率が悪い。

SELECT "topics".* FROM "topics"
SELECT "tags".* FROM "tags" INNER JOIN "topic_tags" ON "tags"."id" = "topic_tags"."tag_id" WHERE "topic_tags"."topic_id" = `topicのidのうち一つ`

N+1

joins

joinsはシンプルに、INNER JOINしてくれる。(内部結合)

INNER JOINなので結合できないデータは捨てられてしまうので、 countなどを使った時に数字が合わないなどの問題が発生する可能性があることに気をつけておく。

left_joinsとすると、LEFT OUTER JOINになる。(外部結合)

joinsはassociationをキャッシュしない。

Category.joins(:topics)
#=> SELECT  `categories`.* FROM `categories` INNER JOIN `topics` ON `topics`.`category_id` = `categories`.`id`

1レコードが1つのオブジェクトにマッピングされるため注意する必要がある。

merge

joinsで結合したテーブルに対して条件式を定義することができる。

Book.joins(:tags).merge(Tag.where(id: [1,2,3])

includes + references

良いとこ取りのincludes 状況によって挙動が変化する。

includesは先読み referencesを付けることで、eager_load と同じ挙動になりLEFT OUTER JOINになる。 OUTER JOIN (外部結合)は片方にしか存在しないレコードも結果に含まれる。 INNER JOINよりも広い範囲でレコードを取得する。

Topic.includes(:tags).references(:tags).where('tags.topic_id < ?', 100)
#=> SELECT  DISTINCT `topics`.`id` FROM `topics` LEFT OUTER JOIN `tags` ON `tags`.`topic_id` = `topics`.`id` WHERE (tags.topic_id < 100)

eager_load

JOINして取得する

指定したassociationをLEFT OUTER JOINで取得し、キャッシュする。 JOINしているので、指定したテーブルに対しての絞り込みが行える。 クエリの数が1つで完結するのでpreloadより早い。

preload

preloadは、指定したassociationを複数のクエリ(SELECT文)に分けてキャッシュする。 指定したテーブルに対しての絞り込みが行えない。行おうとすると例外を投げる。

scope・joins・merge

Topic.joins(:tags).merge(Tag.published)

まとめ

ActiveRecordは、クエリを意識しなくても使えるため便利。 しかし、複雑なクエリや複数のテーブルに関連したデータを取得する際には用意されているメソッドの振る舞いをよく理解しなければパフォーマンス的に問題が生じ、振るまえているものの欲しいデータが取得できていなかったりと苦い経験をすることになるので注意すること。