0
1

目的

ActiveRecordlengthsizeについて、しっかり理解した上で適切に使いこなせるようにする。

補足

本記事は、レコード数の取得においてSQL文の発行数を抑えることの出来るlengthsizeにフォーカスしています。
countメソッドは、必ずSQL文を発行するため割愛(比較用で一部記載あり)

ActiveRecordのlengthとsizeの違い

#size

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

#length

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
  • 既にLoadされていればlengthsizeは同等
  • sizeと比べ、クエリが1回少なく済む(それ以外の場合はsizeの方が効率的)

load_targetメソッド

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メソッド

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 より引用

発行される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 or load or to_aを使用する
      • length:SQL文の発行数や取得データ数によっては有り
      • load:SQL結果の全体を読み込む必要はないためこちらが良さそう
      • to_a:ActiveRecordを配列に変える為、メモリ使用率に注意

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

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

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

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

Arrayのlengthとsizeについて(蛇足)

ちなみに、配列のlengthsizeはエイリアスです。(Array#length
Ruby コーディング規約 は、一応 size の使用を推奨しているようです。

collectよりmap、detectよりfind、find_allよりselect injectよりreduce、lengthよりsizeを好みます。 これは厳しい要件ではありません; もしエイリアスを用いるほうが可読性が上がるのであれば、 使うのもOKです。

参考リンク一覧

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1