目的
ActiveRecordのlength
とsize
について、しっかり理解した上で適切に使いこなせるようにする。
補足
本記事は、レコード数の取得においてSQL文の発行数を抑えることの出来るlength
とsize
にフォーカスしています。
※count
メソッドは、必ずSQL文を発行するため割愛(比較用で一部記載あり)
ActiveRecordのlengthとsizeの違い
#size
# Returns size of the records.
def size
if loaded?
records.length
else
count(:all)
end
end
- Loadされていれば
length
を使用 - LoadされていなければCOUNT関数を用いたクエリでサイズを取得
#length
# 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
- 既にLoadされていれば
length
とsize
は同等 -
size
と比べ、クエリが1回少なく済む(それ以外の場合はsize
の方が効率的)
def load_target
if find_target?
@target = merge_target_lists(find_target, target)
end
loaded!
target
end
# 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 より引用
発行されるSQL文の比較
code | query |
---|---|
Member.all.length | SELECT "members".* FROM "members" |
Member.all.size | SELECT COUNT(*) FROM "members" |
Member.all.count | SELECT COUNT(*) FROM "members" |
- length:全体を読み込んでいる為、sizeより遅い(その代わりキャッシュする)
- size:事前にLoadされていない場合、COUNT関数を使用してSQLを発行する
※キャッシュされているかを判別する場合、loaded?
を使用するのも有効
総括
-
length
- キャッシュを利用して結果を保持する
- 同じオブジェクトに対して再度呼び出した際には、キャッシュされた結果を返す
- つまり、初回の読み込み結果を再利用するのであれば有効
-
size
よりもSQL文の発行数が少ない - 但し、全体を読み込むため、COUNT関数を使用する
size
よりも遅い- その為、大量のデータを扱う場合にはパフォーマンスに注意する
-
size
- 結果をキャッシュに保存しない(キャッシュ参照は行う)
- 事前にLoadされていればその値を参照する
- 事前にLoadされていなければCOUNT関数でSQL文を発行する
- キャッシュしたい場合には、
length
orload
orto_a
を使用する-
length
:SQL文の発行数や取得データ数によっては有り -
load
:SQL結果の全体を読み込む必要はないためこちらが良さそう -
to_a
:ActiveRecordを配列に変える為、メモリ使用率に注意
-
- 結果をキャッシュに保存しない(キャッシュ参照は行う)
キャッシュしておくメリットが無い実装の場合には、size
を使用する
size
を使いつつキャッシュしたい場合には、load
を使用する
大量データの場合はsize
、少量データの場合にはlength
or size
を使用する
キャッシュする場合、初回読み込み時以降は値が変わらなくても問題ないことを考慮した上で使用する
Arrayのlengthとsizeについて(蛇足)
ちなみに、配列のlength
とsize
はエイリアスです。(Array#length)
Ruby コーディング規約 は、一応 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
※本記事に誤りがあれば訂正しますので、お気軽にコメントください