【Rails】なぜcountよりもsizeを使うべきなのか？〜lengthの動きも一緒に確認〜

## 1. はじめに
パフォーマンス向上を考える時、``count``より``size``を使おうという話はよく耳にする。
「``size``だとクエリを毎回発行しないから良いんでしょ」くらいのことは知っていても、実際それぞれのメソッドがどんな挙動をしていて、どう使い分けたら良いかあまり意識せずなんとなくで使っている人は多いのではないだろうか

この記事では実際に3つのメソッドの動きを確認し、「なぜ``count``よりも``size``を使うべき」と言われているのか、また同時にどういう場合にどのメソッドを使ったら良いのかを考えてみたい


## 2. countメソッド
- SQLのCOUNTを使う
- キャッシュの有無に関係なく、毎回クエリを発行する
[GitHub](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/relation/calculations.rb#L43-L53)

```console:console
irb(main):003:0> articles = Article.all
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...

# 1回目　クエリが発行される
irb(main):006:0> articles.count
   (0.1ms)  SELECT COUNT(*) FROM "articles"
=> 2

# 2回目　クエリが発行される
irb(main):007:0> articles.count
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 2

# データをメモリに保存(キャッシュ)
irb(main):008:0> articles.load
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-12-24 14:15:21.338593000 +0000">, #<Article id: 2, title: "Test", text: "fghijk", created_at: "2022-12-24 14:15:39.499115000 +0000", updated_at: "2022-12-24 14:15:39.499115000 +0000">]>

# 3回目　クエリが発行される
irb(main):009:0> articles.count
   (0.1ms)  SELECT COUNT(*) FROM "articles"
=> 2
```
キャッシュがあってもそれを使わず、クエリが発行されていることが分かる

```console:console
# 呼びだせる
irb(main):024:0> Article.count
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 2

# NoMethodErrorとなる
irb(main):025:0> Article.length
Traceback (most recent call last):
        1: from (irb):25
NoMethodError (undefined method `length' for #<Class:0x00007fa685c23340>)

# NoMethodErrorとなる
irb(main):026:0> Article.size
Traceback (most recent call last):
        2: from (irb):25
        1: from (irb):26:in `rescue in irb_binding'
NoMethodError (undefined method `size' for #<Class:0x00007fa685c23340>)
```
唯一モデルから呼び出すことができるのも``count``の特徴である。他の2つから呼び出すとエラーになる


## 3. sizeメソッド
・SQLのCOUNTを使う
・キャッシュがあれば使う
・実行してもメモリ上にデータを乗せることはできない
-> ``length``や``load``等を使ってキャッシュを残す必要がある
[GitHub](https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation.rb#L265-L271)

```console:console
irb(main):001:0> articles = Article.all
   (1.2ms)  SELECT sqlite_version(*)
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...

# 1回目　クエリが発行される
irb(main):002:0> articles.size
   (0.1ms)  SELECT COUNT(*) FROM "articles"
=> 2

# 2回目　クエリが発行される
irb(main):003:0> articles.size
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 2
```
``size``実行時にはキャッシュの保存がされないため、2回目実行した際は再度クエリが発行される

```console:console
# キャッシュを残す
irb(main):004:0> articles.load
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-12-24 14:15:21.338593000 +0000">, #<Article id: 2, title: "Test", text: "fghijk", created_at: "2022-12-24 14:15:39.499115000 +0000", updated_at: "2022-12-24 14:15:39.499115000 +0000">]>
# 3回目　キャッシュを使うのでクエリが発行されない
irb(main):005:0> articles.size
=> 2
```

``load``でキャッシュを残すと、``size``を実行した時にキャッシュが使われるためクエリが発行されていないことが分かる

## 4. lengthメソッド
- SQLの実行結果の行数をカウントする
- キャッシュがあれば使う
- 実行時にメモリにキャッシュが保存される
[GitHub](https://github.com/rails/rails/blob/8e3b1a632c15e8c7f708ab222a81faaad8e3410e/activerecord/lib/active_record/associations/collection_association.rb#L301-L303)

```console:console
irb(main):001:0> articles = Article.all
   (0.9ms)  SELECT sqlite_version(*)
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...

# 1回目　length実行時に、キャッシュが保存される
irb(main):002:0> articles.length
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
=> 2

# 2回目　キャッシュを使う
irb(main):003:0> articles.length
=> 2
```

``length``の場合、実行時にキャッシュが保存されるため、2回目実行時にはそのキャッシュが使われていることが分かる


## 5. 注意しておきたいこと
キャッシュを使うということは、必ずしもそのデータが最新状態とは限らない

```console:console
irb(main):002:0> articles = Article.all
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...
irb(main):003:0> articles.count
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 2
irb(main):004:0> articles.size
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 2
irb(main):005:0> articles.length
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
=> 2

# 新しいデータを追加
irb(main):006:0> Article.create(title: 'Test', text: 'hogehoge')
  TRANSACTION (0.1ms)  begin transaction
  Article Create (0.6ms)  INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Test"], ["text", "hogehoge"], ["created_at", "2022-12-24 15:40:41.200775"], ["updated_at", "2022-12-24 15:40:41.200775"]]
  TRANSACTION (1.4ms)  commit transaction
=> #<Article id: 3, title: "Test", text: "hogehoge", created_at: "2022-12-24 15:40:41.200775000 +0000", updated_at: "2022-12-24 15:40:41.200775000 +0000">

# クエリを実行しているので、最新の件数を取得する
irb(main):007:0> articles.count
   (0.2ms)  SELECT COUNT(*) FROM "articles"
=> 3

# キャッシュを使うので、更新前の件数を取得する
irb(main):008:0> articles.size
=> 2

# キャッシュを使うので、更新前の件数を取得する
irb(main):009:0> articles.length
=> 2

```

上の例のように、新しいデータが追加・削除等された場合に、キャッシュを使う``size``や``length``だと最新状態の件数が取得できるとは限らない。思考停止で``count``より``size``を優先的に使おうとすると、誤った件数を取得してしまうことがあるので、この点は注意が必要である

## 6. なぜcountやlengthよりもsizeを使うべきなのか？
#### countとsizeの対比
``count``は毎回クエリが発行されるため、``size``よりも処理が重くなりがちである。
(特に何万件とデータがある場合は、パフォーマンスの問題が生じることが多い)

#### lengthとsizeの対比
``length``はSQLのCOUNTを使って件数を取得しているのではなく、実行結果の行数を数えてその件数を取得している。処理的にはSQLのCOUNTを使う``count``や``size``方が処理が軽い

#### つまりcountやlengthよりもsizeを使うべき理由は...
``size``は``count``と``length``の良さを併せ持ち、3つの中では一番バランスが取れたメソッドであるから。よって、何か特別な理由がない限り``size``を使うのが良さそうという結論になる。
そうは言ってもこの話は<font color=red>"基本的には"</font>が大前提なので、結論としては以下である


## 7.結論
- データが更新される可能性がない場合には、無駄なクエリ発行を抑えるために``size``を用いる
- データが更新される可能性がある場合には、正しい件数を数えるために``count``を用いる
- ``length``はキャッシュの保存も一緒に実行すべき時に用いる(既にキャッシュが存在し、件数を数えるだけみたいな時やその後キャッシュを使う予定がない場合は使わない)
- 上記の通り、どのような場合も``count``より``size``を使うべきというわけではなく、その場に適したメソッドを取捨選択することが必要である


## 参考
https://mgre.co.jp/blog/3199
https://himakuro.com/rails-count-length-size-guide
https://ohbarye.hatenablog.jp/entry/2021/07/03/002817
https://medium.com/ruby-daily/comparing-count-length-and-size-in-ruby-on-rails-e061840bc19e