Active Recordのcount, length, sizeの違いを理解して使い分ける

## はじめに
以下の3つはいずれもActive Recordのコレクションに含まれるオブジェクトの数を返すメソッドです。

- `count`メソッド
- `length`メソッド
- `size`メソッド

しかしこれらはすべて内部的な挙動が異なるため、違いを理解して使い分ける必要があります。

そこでこの記事ではこれら３メソッドの挙動の違いと使い分けについてまとめます。

この記事におけるRailsのバージョンは**Rails7.1**です。

## count, size, lengthの違い
まず最初に結論です。
- **`count`メソッド:** `COUNT(*)`のSQLを発行し、データベース側でカウントを実行する
- **`size`メソッド:** オブジェクトがメモリにロードされているならSQLを発行せずにメモリ上のオブジェクトの数を返す。ロードされていない場合は`count`メソッドと同様のSQLを発行する
- **`length`メソッド:** オブジェクトがメモリにロードされていない場合はレコードをメモリにロードしてからカウントを行う。すでにロードされている場合は新たなSQLを発行せずメモリ上のオブジェクトの数を返す


実際に発行されるSQLを見た方がイメージしやすいかもしれません。

`count`メソッド：
```sql
-- COUNT(*)を発行しデータベース側でカウントする
SELECT COUNT(*) FROM table_name WHERE conditions
```

`size`メソッド:
```sql
-- オブジェクトがメモリにロードされている場合はSQLを発行せずにメモリ上のオブジェクト数を返す
-- オブジェクトがメモリにロードされていない場合は`count`メソッドと同じSQLを発行
SELECT COUNT(*) FROM table_name WHERE conditions
```

`length`メソッド:
```sql
-- オブジェクトがメモリにロードされている場合はSQLを発行せずにメモリ上のオブジェクト数を返す
-- オブジェクトがメモリにロードされていない場合は全レコードを取得し、メモリにロードしてからカウントする
SELECT * FROM table_name WHERE conditions
```

## count, size, lengthの使い分け
これらのメソッドの挙動の違いを理解したところで、それぞれのメソッドをどのように使い分けるべきかを考えてみます。

大まかに言えば次のような判断基準でよいと思いました。

- データベース上のレコード数のみが必要で、オブジェクトの内容は不要 → `count`メソッド
- 後続の処理でActive Recordオブジェクトの配列を使用する → `length`メソッド
- 幅広いケースに柔軟に対応したい → `size`メソッド

`size`メソッドはオブジェクトの取得状況に応じた柔軟な対応をしてくれるため、多くの場面で効率的に動作すると思われます。

次にそれぞれの使い分けを具体的なコードでも確認します。

### `count`メソッドの使用例
下記の例では`Post`オブジェクトの件数を取得後、後続の処理で`Post`オブジェクトの配列である`posts`を使っていません。

このように件数だけが必要でオブジェクトの内容が不要な場合、`count`メソッドが効率的です。

大量のレコードがある場合でも、データベース側でカウントを行うため、メモリ効率の点で優れています。

```ruby
posts = Post.all
post_count = posts.count　　# SELECT COUNT(*) FROM "posts" が発行
puts "投稿数: #{post_count}"
```

### `size`メソッドの使用例
下記の例だと`posts.each`の時点でオブジェクトがメモリにロードされています。

そのため`posts.size`とすることで、SQLが再び発行されることを防げます。

これは`posts.length`にした場合も同様です。

```ruby
posts = Post.all
posts.each { |post| puts post.title }  # SELECT "posts".* FROM "posts" が発行
post_count = posts.size  # posts.eachでオブジェクトがロードされているため、SQLは発行されない 
```

逆に`posts.count`としてしまうと、`posts.each`でオブジェクトを取得しているにも関わらず、件数を取得するためのSQLが再度発行されてしまうため非効率です。

```ruby
posts = Post.all
posts.each { |post| puts post.title }  # SELECT "posts".* FROM "posts" が発行
post_count = posts.count  # SELECT COUNT(*) FROM "posts" が発行されてしまう
```

### `length`メソッドの使用例
下記の例では`posts.length`の直後の`posts.each`で各`post`オブジェクトが利用されます。

そのため事前に`posts.length`でロードしておけば`posts.each`ではそのロードされたオブジェクトを利用可能です。

```ruby
posts = Post.all
post_count = posts.length  # SELECT "posts".* FROM "posts" を発行
posts.each { |post| puts post.title }  # すでにロードされているため、新たなSQLは発行されない
```

このケースで`length`の代わりに`posts.count`や`posts.size`とするのは非効率です。

`count`と`size`は件数のみを取得するため、`posts.each`でオブジェクトの内容を取得するSQLを再度発行しなくてはならないからです。

```ruby
posts = Post.all
post_count = posts.count  # SELECT COUNT(*) FROM "posts" を発行し、件数だけを取得
posts.each { |post| puts post.title }  # SELECT "posts".* FROM "posts" を発行しないといけない
```

後続の処理でオブジェクトの内容も必要になるのであれば、最初から`posts.length`を使っておくのが効率的です。

ただし、大量のレコードがある場合に`length`メソッドを使ってしまうとパフォーマンス上の問題を引き起こしかねません。

上記のケースで`posts`が100万件あったと仮定すると、`posts.length`の実行で100万件の`Post`オブジェクトがメモリに作成されてしまい、メモリ使用量が大きくなってしまうからです。

こういった場合は`count`メソッドを使ってデータベース側でカウントを行うか、`limit`を使用してロードするレコード数を制限することでメモリ使用量を抑えることができます。

```ruby
# 100万件のレコードがあるとします
posts = Post.all
post_count = posts.length  # このメソッド呼び出しで100万件のレコードがメモリにロードされます


# 改善例1: countメソッドを使用
posts = Post.all
post_count = posts.count  # データベース側でカウント

# 改善例2: limitを使用（一部のレコードだけが必要な場合）
posts = Post.limit(20)
posts.length  # 最大20件のみメモリにロード
```

## 注意: size, lengthは最新の状態を反映しないことがある
`size`メソッドや`length`メソッドは最新のデータベースの状態を反映しないことがあります。

オブジェクトがすでにメモリにロードされている場合、`size`と`length`は新しいSQLを発行しないからです。

実際のコードで説明します。

```ruby
rails-app> posts.size
  Post Count (0.3ms)  SELECT COUNT(*) FROM "posts"
=> 8
rails-app> posts.length
  Post Load (0.7ms)  SELECT "posts".* FROM "posts"
=> 8
rails-app> posts.count
  Post Count (0.4ms)  SELECT COUNT(*) FROM "posts"
=> 8
rails-app> Post.create(content: "New Content")
  TRANSACTION (0.1ms)  begin transaction
  Post Create (2.2ms)  INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id"  [["content", "New Content"], ["created_at", "2024-08-10 01:10:37.192066"], ["updated_at", "2024-08-10 01:10:37.192066"]]
  TRANSACTION (0.8ms)  commit transaction
=> #<Post:0x000000012d71aa10 id: 9, content: "New Content", created_at: "2024-08-10 01:10:37.192066000 +0000", updated_at: "2024-08-10 01:10:37.192066000 +0000">
rails-app> posts.size
=> 8
rails-app> posts.length
=> 8
rails-app> posts.count
  Post Count (4.6ms)  SELECT COUNT(*) FROM "posts"
=> 9
rails-app> posts.size
=> 8
rails-app> posts.length
=> 8
```

`Post.create`で新しいレコードを作成した後も、`size`メソッドと`length`メソッドはSQLを発行せず、同じ値を返しています。

すでにメモリにロードされているオブジェクトを参照しているからです。

一方で`count`メソッドは`SELECT COUNT(*) FROM "posts"`で最新の件数を取得しています。

このように、`size`メソッドと`length`メソッドは必ずしも最新のデータベースの状態を反映するわけではないため、注意しましょう。

## おわりに
最後におさらいです。

- **`count`メソッド:** `COUNT(*)`のSQLを発行し、データベース側でカウントを実行する
- **`size`メソッド:** オブジェクトがメモリにロードされているならSQLを発行せずにメモリ上のオブジェクトの数を返す。ロードされていない場合は`count`メソッドと同様のSQLを発行する
- **`length`メソッド:** オブジェクトがメモリにロードされていない場合はレコードをメモリにロードしてからカウントを行う。すでにロードされている場合は新たなSQLを発行せずメモリ上のオブジェクトの数を返す

一見ただのエイリアスメソッドに思えますが、内部的な挙動はだいぶ異なるため意識して使い分けましょう。

また記事に関して誤りがありましたらご指摘いただけますと幸いです。

## 参考資料
https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/calculations.rb#L94

https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation.rb#L346

https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/delegation.rb#L101

https://tech.smarthr.jp/entry/2021/11/11/151444

https://medium.com/unagi/ruby-on-rails-length-vs-size-vs-count-with-examples-a32e17274c79

https://mgre.co.jp/blog/3199