includes を利用しても n+1 が発生する場合があるのでそれの抑制方法

## 結論

`includes`を利用してN+1を抑制して関連するオブジェクトの個数を取得したい時は`size`を使おう。


## 具体例

下記のような構造を持ったモデルクラスがある。

```rb
class Series < ApplicationRecord
  has_many :books, dependent: :destroy
end

class Book < ApplicationRecord
  belongs_to :series
end
```

複数の`Series`と、それに紐づく`Book`の数を取得しようとするとN+1が発生する。

```irb
irb(main):013:0> Series.where('id < 20').select{|s|s.books.size < 20}
D, [2018-02-14T17:15:43.433778 #4] DEBUG -- :   Series Load (1.2ms)  SELECT "series".* FROM "series" WHERE (id < 20)
D, [2018-02-14T17:15:43.436488 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 1]]
D, [2018-02-14T17:15:43.438897 #4] DEBUG -- :    (1.2ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 2]]
D, [2018-02-14T17:15:43.440871 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 3]]
...
...
...
D, [2018-02-14T17:15:43.469686 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 17]]
D, [2018-02-14T17:15:43.471588 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 18]]
D, [2018-02-14T17:15:43.473640 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 19]]
```


N+1を抑制するために`includes`を利用する。

```irb
irb(main):014:0> Series.where('id < 20').includes(:books).select{|s|s.books.size < 20}
D, [2018-02-14T17:15:46.393076 #4] DEBUG -- :   Series Load (1.2ms)  SELECT "series".* FROM "series" WHERE (id < 20)
D, [2018-02-14T17:15:46.398703 #4] DEBUG -- :   Book Load (3.8ms)  SELECT "books".* FROM "books" WHERE "books"."series_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
```

しかし、`Book`を数えるのに`count`メソッドを利用すると、`COUNT`のクエリが発行されてしまい、N+1が解消できない。


```irb
irb(main):015:0> Series.where('id < 20').includes(:books).select{|s|s.books.count < 20}
D, [2018-02-14T17:15:59.374879 #4] DEBUG -- :   Series Load (1.0ms)  SELECT "series".* FROM "series" WHERE (id < 20)
D, [2018-02-14T17:15:59.379237 #4] DEBUG -- :   Book Load (2.8ms)  SELECT "books".* FROM "books" WHERE "books"."series_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
D, [2018-02-14T17:15:59.415829 #4] DEBUG -- :    (0.8ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 1]]
D, [2018-02-14T17:15:59.417230 #4] DEBUG -- :    (0.6ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 2]]
...
...
...
D, [2018-02-14T17:15:59.437119 #4] DEBUG -- :    (0.7ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 17]]
D, [2018-02-14T17:15:59.438352 #4] DEBUG -- :    (0.6ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 18]]
D, [2018-02-14T17:15:59.439596 #4] DEBUG -- :    (0.6ms)  SELECT COUNT(*) FROM "books" WHERE "books"."series_id" = $1  [["series_id", 19]]
```


## 考察

`size`メソッドは内部的に`Array`のメソッドを呼び出している。
対して`count`メソッドは`count`クエリを発行するような処理となっている。
よって、`count`メソッドを呼び出すと`includes`を無視してそれぞれのクエリが発行されてしまい、`N+1`となる。




## 参考
https://github.com/rails/rails/blob/6df9b69b2363734175d8869393e83e6ce8b4d7b6/activerecord/lib/active_record/associations/collection_association.rb#L209
https://github.com/rails/rails/blob/6df9b69b2363734175d8869393e83e6ce8b4d7b6/activerecord/lib/active_record/relation/calculations.rb#L39