似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ


Activerecordを使ってるとき、関連(Association)のあるmodel同士をまとめて取得したい時がけっこうある。そんな時、`includes`や`joins`を使えば効率良くデータを取得出来るんだけど、実はこの二つは振る舞いや特徴が全然違ってたりする。ややこしい気がしたので、ここでちょっとまとめておく。

先に結論を書いておくと、基本的には

- `includes`は先読みしてキャッシュしておく。
- `joins`はただINNER JOINしてくれる。

と思っておけばOK。

ちなみに、railsのversionは4.1.0。Web上に落ちてる情報は古いせいか若干現状の挙動とは違ってたりしたので、気をつけた方が良さそう。

# includes
`includes`はデータの先読みをしてくれる。その為、関連modelに対する参照を持ちたい場合に使う。そう言われてもよくわからないと思うので、実際に使用例を見てみる。

`tagging`が中間テーブルとなり、`blog`と`tag`の間に多対多の関係が出来ているとする。

この時、

```ruby:ruby
Blog.includes(:tags).each { |blog| p blog.tags.map(&:name) }
```

の様なincludesを含んだコードを実行すると、

```postgres:postgresql
SELECT "blogs".* FROM "blogs"
SELECT "taggings".* FROM "taggings"  WHERE "taggings"."blog_id" IN (`取得したblogsのID`)
SELECT "tags".* FROM "tags"  WHERE "tags"."id" IN (`取得したtaggingのid`)
```

というSQLが発行される。

`blogs, taggings, tags`へのsqlの発行がそれぞれ1回ずつ行われている事が分かる。`blogs`を取得し、取得した`blogs`に相当する`tagging`を取得し、さらに取得した`tagging`に相当する`tag`を取得するといった具合である。

当然の挙動とおもうかも知れないが、`includes(:tags)`しなかった場合にはこうはいかない。`blog`から`tags`への参照が発生するたびに新規のsqlを発行してtagsを取得しようとしてしまうため、いわゆる ***N+1問題*** が発生する。これは大変効率が悪い....!!!

```ruby:ruby
Blog.each { |blog| p blog.tags.map(&:name) }
```
```postgres:postgresql
SELECT "blogs".* FROM "blogs"
SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."blog_id" = `blogのidのうち一つ`
// このtagsへのクエリが取得したblogの数N回だけ発行される!!!
```

そんな訳で、`includes`は別のmodelへの参照が必要となる場合にN+1問題を避ける為に使われる。

# joins
`joins`の仕組みはめちゃくちゃシンプルで、ただINNER JOINしてくれる。

```ruby:ruby
Blog.joins(:tags)
```
```postgres:postgresql
SELECT "blogs".* FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id"
```

`joins`を使えば、joins先のテーブルのカラムの値を使って絞り込んだり出来る。

```ruby:ruby
Blog.joins(:tags).where(tags: {id: [*1..3]})
```
```postgres:postgresql
SELECT "blogs".* FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" AND INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (1, 2, 3)
```

注意しなければいけないのは、INNER JOINした結果のレコード1つ1つが1つのオブジェクトにマッピングされて返ってくる事。当然、返ってくるレコードの数は違ってくる。

```ruby:ruby,postgresql
Blog.all.size
SELECT COUNT(*) FROM "blogs"
=> 2923

Blog.includes(:tags).all.size
SELECT COUNT(*) FROM "blogs"
=> 2923

Blog.joins(:tags).all.size
SELECT COUNT(*) FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id"  INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id"
=> 38943
```

`blog`のレコード数自体が`2923`なのに対して、`joins(:tags)`してINNER JOINするとレコード数が`38943`まで増えている事が分かる。

それから、`joins`したテーブルのカラムにアクセスするには、明示的に`select`をしてやらなければならない。

```ruby:ruby
Blog.first.attributes
=> {"id"=>1,
 "title"=>"眠りに恐怖を感じる「睡眠恐怖症」。難病患者の苦しみに見る社会の闇。"}
```
```postgres:postgresql
SELECT  "blogs".* FROM "blogs"   ORDER BY "blogs"."id" ASC LIMIT 1
```

```ruby:ruby
Blog.joins(:tags).select('blogs.*, tags.name').first.attributes
=> {"id"=>1,
 "title"=>"眠りに恐怖を感じる「睡眠恐怖症」。難病患者の苦しみに見る社会の闇。",
 "name"=>"睡眠"}
```
```postgres:postgresql
SELECT  blogs.*, tags.name FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id"  ORDER BY "blogs"."id" ASC LIMIT 1 
```

1レコードが1つのオブジェクトにマッピングされるため、取得したオブジェクトの`name`プロパティに`tag`の`name`が格納されているのが特徴的。`includes`と違って先読みはしていない為、`tags`への参照はSQLの発行を意味する事にも気をつけなければならない。

# ちょっと細かい話。includes + referencesで先読みしつつ絞り込みも行う。
`includes`は先読みと書いたが、`references`を付ける事でLEFT OUTER JOINにもなる。OUTER JOINは片方にしか存在しないレコードも結果に含めるJOINで、INNER JOINよりも広い範囲でレコードを取得する。

```ruby:ruby
Blog.includes(:tags).references(:tags).where('tags.id < ?', 3)
```
```postgres:postgresql
SELECT "blogs"."id" AS t0_r0, "blogs"."title" AS t0_r1, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1 FROM "blogs" LEFT OUTER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE (tags.id < 3)
```

`size`でレコード数を見ると、元の`includes`の処理に合わせてblogsの重複を無くした数を返してくれる(その為に`DISTINCT`が使われている。)。

```ruby:ruby
Blog.includes(:tags).references(:tags).where('tags.id < ?', 3).size
```
```postgres:postgresql
SELECT COUNT(DISTINCT "blogs"."id") FROM "blogs" LEFT OUTER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE (tags.id < 3)
=> 97
```

`joins`の様にJOIN先のテーブルのカラムを使って絞り込む事が出来るのも特徴的で、上記の例ではどちらも`tags`の`id`が[1, 2, 3]に含まれる様な`blogs`だけを返している。

先読みの機能も残してくれていて、`blog`から`tags`への参照も余分なクエリを発行する事無く行える。
 
```ruby:ruby
Blog.includes(:tags).references(:tags).where('tags.id < ?', 3).each do |blog|
  p "#{blog.title}, #{blog.tags.first}"
end
```
```postgres:postgresql
SELECT "blogs"."id" AS t0_r0, "blogs"."title" AS t0_r1, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1 FROM "blogs" LEFT OUTER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE (tags.id < 3)
```

元の`includes`と`joins`の良いとこどり見たいな感じで、すごい。

ただ、気をつけなければいけないのは`limit`つけた場合で、DISTINCTを使って決められたレコード数の`blog`を取得してからOUTER JOINのクエリを発行しようとする。このDISTINCTの処理がなかなか重かったりするので、乱用は良く無いと思った。

```ruby:ruby
Blog.includes(:tags).references(:tags).where('tags.id in (?)', [*100..200]).limit(100)
```
```postgres:postgresql
SELECT  DISTINCT "blogs"."id" FROM "blogs" LEFT OUTER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`100から200`) LIMIT 100

SELECT "blogs"."id" AS t0_r0, "blogs"."title" AS t0_r1, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1 FROM "blogs" LEFT OUTER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`100から200`) AND "blogs"."id" IN (`取得したblogのid`)
```

## LEFT OUTER JOINのちょっとオシャレな方法
where句の条件として文字列を使わない場合には、`references`を付けなくてもLEFT OUTER JOINが出来る。

```ruby:ruby
Blog.includes(:tags).where(tags: {id: [*100..200]})
```
```postgres:postgresql
SELECT "rss_feeds"."id" AS t0_r0, "rss_feeds"."title" AS t0_r1, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1 FROM "rss_feeds" LEFT OUTER JOIN "taggings" ON "taggings"."rss_feed_id" = "rss_feeds"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`100から200`)
```

この方がオシャレ感が出て良いかもしれない。

# includes + joinsで先読みしつつ絞り込み。
INNER JOINの場合でも、`includes + joins`で先読みしつつJOIN先の条件で絞り込みが出来る。`includes + references`使った時との違いは、`size`でDISTINCTがかからない事。

```ruby:ruby
Blog.includes(:tags).joins(:tags).where(tags: {id: [*100..200]}).limit(100)
```
```postgres:postgresql
SELECT  DISTINCT "blogs"."id" FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`100から200`) LIMIT 100

SELECT "blogs"."id" AS t0_r0, "blogs"."title" AS t0_r1, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1 FROM "blogs" INNER JOIN "taggings" ON "taggings"."blog_id" = "blogs"."id" INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`取得されたtagsのid`)
```

```ruby:ruby
Blog.includes(:tags).joins(:tags).where(tags: {id: [*100..200]}).size
```
```postgres:postgresql
SELECT COUNT(*) FROM "blogs" INNER JOIN "taggings" ON "taggings"."blogs_id" = "blogs"."id" INNER JOIN "tags" ON "tags"."id" = "taggings"."tag_id" WHERE "tags"."id" IN (`100から200`)
=> 74
```

`includes+references`よりも`includes+joins`の方が使用頻度は高いかもしれない。

# まとめ
Activerecordでは、関連するモデルを効率良く取得する為の手段として`includes`や`joins`メソッドが提供されている。ただ参照先を利用したいなら`includes`, INNER JOINして絞り込みたいなら`joins`, 絞り込みつつ参照先も利用したいなら`includes + references`や`includes + joins`を使えば良い。

と言いつつ、一番大事なのは実際に発行されたSQLと処理にかかった時間を見て判断する事だったりするので、ちょっと複雑なSQL書きたい時はきちんと確認する癖をつけると良いのかなと思う。