📐
Rubyにおけるsize、length、countの使い分けについて
Rubyで要素数を数える際、size
、length
、count
という3つのメソッドがあります。
これまで何も気にせずに気分で使用していましたが、これらの使い分けは、特にActiveRecordを使用する際に重要になります。
また、acts_as_paranoid
Gemを使用している場合は、さらに考慮すべき点があるため、まとめてみました。
参考:Ruby公式ドキュメント
1. size、length、countの基本的な違い
size
- メモリ上のオブジェクトに対して動作する
- 配列やハッシュの場合、要素数をすぐに返す
- ActiveRecordの場合、可能であればキャッシュされた結果を返し、それができない場合はデータベースにCOUNTクエリを発行する
length
- 基本的に
size
と同じ動作をする - 配列やStringクラスでよく使用される
count
- 常にデータベースにCOUNTクエリを発行する
- ブロックを渡すことで、条件付きのカウントが可能
参考:
Ruby Array#size
Ruby Array#length
ActiveRecord::Calculations#count
2. ActiveRecordでの使い分け
sizeを使う場合
a. データが既にメモリにロードされている場合
「メモリにロード」とは、データをコンピュータの主記憶装置(RAM)に読み込むことを指す
つまりデータをデータベースから取り出し、Rubyのオブジェクトとしてプログラムが直接アクセスできる状態
users = User.all.to_a # すべてのユーザーをメモリにロード
users.size # メモリ上のオブジェクトの数を返す(データベースアクセスなし)
この場合、size
メソッドはメモリ上のオブジェクトの数を直接カウントするため、非常に高速
b. 結果がキャッシュされている可能性が高い場合
ActiveRecordは、特定の条件下で結果をキャッシュする
users = User.all
users.size # 初回:データベースにクエリを発行
users.size # 2回目以降:キャッシュされた結果を返す(データベースアクセスなし)
countを使う場合
- データベース上の正確な数が必要な場合
- 条件付きのカウントが必要な場合
User.count # データベースにCOUNTクエリを発行
User.count { |u| u.age > 18 } # 条件付きカウント
参考:ActiveRecord::Calculations#count
3. acts_as_paranoidを使用している場合の注意点
acts_as_paranoid
Gemは論理削除を実装するためのもの
このGemを使用している場合、以下の点に注意が必要
- デフォルトのスコープが変更され、削除されていないレコードのみを返す
- 論理削除されたレコードを含めてカウントしたい場合は、特別な処理が必要
class User < ApplicationRecord
acts_as_paranoid
end
# 削除されていないユーザーのみをカウント
User.count # または User.size
# 論理削除されたユーザーも含めてカウント
User.with_deleted.count
# 論理削除されたユーザーのみをカウント
User.only_deleted.count
acts_as_paranoidを使っているときに、countとsizeで異なる結果が出る理由〜図書館を例に〜
[設定]
あなたは図書館の管理者
図書館には10冊の本がある
そのうち3冊は「貸出中」としてマークされている
acts_as_paranoid
を使う
- 通常の本 = 削除されていないレコード
- 貸出中の本 = 論理削除されたレコード(実際には削除されていない)
count
、size
2つの方法で本の数を数えてみる
-
count
:「棚にある本だけを数える」- 貸出中の本(論理削除されたレコード)は数えない
- 結果:7冊(10冊 - 貸出中の3冊)
-
size
:「図書館のシステムに登録されている全ての本を数える」- 棚にある本も貸出中の本も全て数える
- 結果:10冊(全ての本)
なぜこの違いが起こるのか
-
count
は常にデータベースに「棚にある本は何冊?」と尋ねる -
size
は、まず「既に全ての本のリストを持っているか」を確認する- もし持っていれば、そのリストの長さを返す(この場合は10冊)
- 持っていなければ、
count
と同じように「棚にある本は何冊?」と尋ねる
class Book < ApplicationRecord
acts_as_paranoid
end
# 全ての本(貸出中も含む)をメモリに読み込む
all_books = Book.with_deleted.to_a
# `count`メソッドは常にデータベースに問い合わせるため、
# デフォルトで貸出中の本(論理削除されたレコード)を除外した7を返す
Book.count # => 7 (貸出中の本を除外)
# `all_books`に全ての本(貸出中も含む)が読み込まれているため、
# `size`メソッドは10を返す
all_books.size # => 10 (全ての本を含む)
4. パフォーマンスの考慮
-
size
は、多くの場合でパフォーマンスが最も良い(特にデータが既にメモリにある場合や結果がキャッシュされている場合) -
count
は常にデータベースクエリを発行するため、大規模なデータセットでは遅くなる可能性がある -
acts_as_paranoid
を使用している場合、with_deleted
やonly_deleted
スコープを使用すると、追加のクエリが発生する可能性がある
参考:Rails Performance: Counting with Counter Cache
5. まとめ
- 一般的には
size
を使用し、必要に応じてcount
を使うのが良さそう - データが既にメモリにロードされている場合や、結果がキャッシュされている可能性が高い場合は、
size
を使用すると高速に処理できる - 最新のデータベースの状態を反映した正確な数が必要な場合は
count
を使用する -
acts_as_paranoid
を使用している場合は、論理削除されたレコードを含めるかどうかを明示的に指定する必要がある- 論理削除を考慮して正確な件数を得たい場合は、
.with_deleted.count
を使用するのが最も確実で明示的な方法
- 論理削除を考慮して正確な件数を得たい場合は、
- どのメソッドを使用するかは、具体的な使用ケースやアプリケーションの要件によって判断が必要
Discussion