【ActiveRecord】length vs size

# 目的
**ActiveRecord**の`length`と`size`について、しっかり理解した上で適切に使いこなせるようにする。
#### 補足
本記事は、レコード数の取得においてSQL文の発行数を抑えることの出来る`length`と`size`にフォーカスしています。
※`count`メソッドは、必ずSQL文を発行するため割愛（比較用で一部記載あり）


# ActiveRecordのlengthとsizeの違い

## `#size`
https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation.rb#L345-L352
```rb:rails/activerecord/lib/active_record/relation.rb
# Returns size of the records.
def size
  if loaded?
    records.length
  else
    count(:all)
  end
end
```
:::note
- Loadされていれば`length`を使用
- LoadされていなければCOUNT関数を用いたクエリでサイズを取得
:::

## `#length`
https://github.com/rails/rails/blob/8e3b1a632c15e8c7f708ab222a81faaad8e3410e/activerecord/lib/active_record/associations/collection_association.rb#L296-L303

```rb:rails/activerecord/lib/active_record/associations/collection_association.rb
# Returns the size of the collection calling +size+ on the target.
#
# If the collection has been already loaded +length+ and +size+ are
# equivalent. If not and you are going to need the records anyway this
# method will take one less query. Otherwise +size+ is more efficient.
def length
  load_target.size
end
```

:::note
- 既にLoadされていれば`length`と`size`は同等
- `size`と比べ、クエリが1回少なく済む（それ以外の場合は`size`の方が効率的）
:::

[load_targetメソッド](https://github.com/rails/rails/blob/8e3b1a632c15e8c7f708ab222a81faaad8e3410e/activerecord/lib/active_record/associations/collection_association.rb#L377-L384)
```rb:rails/activerecord/lib/active_record/associations/collection_association.rb
def load_target
  if find_target?
    @target = merge_target_lists(find_target, target)
  end

  loaded!
  target
end
```


[lengthメソッドが参照しているsizeメソッド](https://github.com/rails/rails/blob/8e3b1a632c15e8c7f708ab222a81faaad8e3410e/activerecord/lib/active_record/associations/collection_association.rb#L269-L294)
```rb:rails/activerecord/lib/active_record/associations/collection_association.rb
# Returns the size of the collection by executing a SELECT COUNT(*)
# query if the collection hasn't been loaded, and calling
# <tt>collection.size</tt> if it has.
#
# If the collection has been already loaded +size+ and +length+ are
# equivalent. If not and you are going to need the records anyway
# +length+ will take one less query. Otherwise +size+ is more efficient.
#
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
  if !find_target? || loaded?
    if association_scope.distinct_value
      target.uniq.size
    else
      target.size
    end
  elsif !loaded? && !association_scope.group_values.empty?
    load_target.size
  elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
    unsaved_records = target.select(&:new_record?)
    unsaved_records.size + count_records
  else
    count_records
  end
end
```


# もっと分かりやすく

## 早見表
|メソッド名  |キャッシュ参照  |キャッシュ保存 |COUNT関数 |
|---|---|---|---|
|length|⭕️|⭕️|❌|
|size|⭕️|❌|⭕️|
|count|❌|❌|⭕️|

※[[Ruby on Rails] 数えるメソッドの違い length, size, count](https://yorutaru097.hatenadiary.jp/entry/2022/04/22/112545) より引用

## 発行されるSQL文の比較
|code  |query  |
|---|---|
|Member.all.length|SELECT "members".* FROM "members"|
|Member.all.size|SELECT COUNT(*) FROM "members"|
|Member.all.count|SELECT COUNT(*) FROM "members"|

:::note
- length：全体を読み込んでいる為、sizeより遅い（その代わりキャッシュする）
- size：事前にLoadされていない場合、COUNT関数を使用してSQLを発行する
:::

※キャッシュされているかを判別する場合、`loaded?`を使用するのも有効

## 総括
- `length`
  - キャッシュを利用して結果を保持する
  - 同じオブジェクトに対して再度呼び出した際には、キャッシュされた結果を返す
    - つまり、初回の読み込み結果を再利用するのであれば有効
  - `size`よりもSQL文の発行数が少ない
  - 但し、全体を読み込むため、COUNT関数を使用する`size`よりも遅い
    - その為、<INS>大量のデータを扱う場合にはパフォーマンスに注意する</INS>
- `size`
  - 結果をキャッシュに保存しない（キャッシュ参照は行う）
    - 事前にLoadされていればその値を参照する
    - 事前にLoadされていなければCOUNT関数でSQL文を発行する
  - キャッシュしたい場合には、`length` or `load` or `to_a`を使用する
    - `length`：SQL文の発行数や取得データ数によっては有り
    - `load`：SQL結果の全体を読み込む必要はないためこちらが良さそう
    - `to_a`：ActiveRecordを配列に変える為、メモリ使用率に注意

:::note
キャッシュしておくメリットが無い実装の場合には、`size`を使用する
:::

:::note
`size`を使いつつキャッシュしたい場合には、`load`を使用する
:::

:::note
大量データの場合は`size`、少量データの場合には`length` or `size`を使用する
:::

:::note warn
キャッシュする場合、初回読み込み時以降は値が変わらなくても問題ないことを考慮した上で使用する
:::


# Arrayのlengthとsizeについて（蛇足）
ちなみに、配列の`length`と`size`はエイリアスです。（[Array#length](https://docs.ruby-lang.org/ja/latest/method/Array/i/length.html)）
[Ruby コーディング規約](https://github.com/takayama-daisuke/ruby-style-guide) は、一応 `size` の使用を推奨しているようです。
>collectよりmap、detectよりfind、find_allよりselect injectよりreduce、lengthよりsizeを好みます。 これは厳しい要件ではありません; もしエイリアスを用いるほうが可読性が上がるのであれば、 使うのもOKです。


# 参考リンク一覧
- https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation.rb
- https://github.com/rails/rails/blob/8e3b1a632c15e8c7f708ab222a81faaad8e3410e/activerecord/lib/active_record/associations/collection_association.rb
- https://mgre.co.jp/blog/3199
- https://qiita.com/diskshima/items/dc9aa0c3f131971365bc
- https://qiita.com/ginger-yell/items/d8c05fc312b820bbc230
- https://yorutaru097.hatenadiary.jp/entry/2022/04/22/112545
- https://www.okb-shelf.work/entry/activerecord_count_record
- https://docs.ruby-lang.org/ja/latest/method/Array/i/length.html
- https://github.com/takayama-daisuke/ruby-style-guide



※本記事に誤りがあれば訂正しますので、お気軽にコメントください