📐

Rubyにおけるsize、length、countの使い分けについて

2024/10/01に公開

Rubyで要素数を数える際、sizelengthcountという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回目以降:キャッシュされた結果を返す(データベースアクセスなし)

参考:ActiveRecord::Relation

countを使う場合

  • データベース上の正確な数が必要な場合
  • 条件付きのカウントが必要な場合
User.count  # データベースにCOUNTクエリを発行
User.count { |u| u.age > 18 }  # 条件付きカウント

参考:ActiveRecord::Calculations#count

3. acts_as_paranoidを使用している場合の注意点

acts_as_paranoid Gemは論理削除を実装するためのもの
このGemを使用している場合、以下の点に注意が必要

  1. デフォルトのスコープが変更され、削除されていないレコードのみを返す
  2. 論理削除されたレコードを含めてカウントしたい場合は、特別な処理が必要
class User < ApplicationRecord
  acts_as_paranoid
end

# 削除されていないユーザーのみをカウント
User.count  # または User.size

# 論理削除されたユーザーも含めてカウント
User.with_deleted.count

# 論理削除されたユーザーのみをカウント
User.only_deleted.count

参考:acts_as_paranoid gem

acts_as_paranoidを使っているときに、countとsizeで異なる結果が出る理由〜図書館を例に〜

[設定]
あなたは図書館の管理者
図書館には10冊の本がある
そのうち3冊は「貸出中」としてマークされている

acts_as_paranoidを使う

  • 通常の本 = 削除されていないレコード
  • 貸出中の本 = 論理削除されたレコード(実際には削除されていない)

countsize2つの方法で本の数を数えてみる

  1. count:「棚にある本だけを数える」

    • 貸出中の本(論理削除されたレコード)は数えない
    • 結果:7冊(10冊 - 貸出中の3冊)
  2. 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_deletedonly_deletedスコープを使用すると、追加のクエリが発生する可能性がある

参考:Rails Performance: Counting with Counter Cache

5. まとめ

  • 一般的にはsizeを使用し、必要に応じてcountを使うのが良さそう
  • データが既にメモリにロードされている場合や、結果がキャッシュされている可能性が高い場合は、sizeを使用すると高速に処理できる
  • 最新のデータベースの状態を反映した正確な数が必要な場合はcountを使用する
  • acts_as_paranoidを使用している場合は、論理削除されたレコードを含めるかどうかを明示的に指定する必要がある
    • 論理削除を考慮して正確な件数を得たい場合は、.with_deleted.countを使用するのが最も確実で明示的な方法
  • どのメソッドを使用するかは、具体的な使用ケースやアプリケーションの要件によって判断が必要

Discussion