はじめに
以下の3つはいずれもActive Recordのコレクションに含まれるオブジェクトの数を返すメソッドです。
-
count
メソッド -
length
メソッド -
size
メソッド
しかしこれらはすべて内部的な挙動が異なるため、違いを理解して使い分ける必要があります。
そこでこの記事ではこれら3メソッドの挙動の違いと使い分けについてまとめます。
この記事におけるRailsのバージョンはRails7.1です。
count, size, lengthの違い
まず最初に結論です。
-
count
メソッド:COUNT(*)
のSQLを発行し、データベース側でカウントを実行する -
size
メソッド: オブジェクトがメモリにロードされているならSQLを発行せずにメモリ上のオブジェクトの数を返す。ロードされていない場合はcount
メソッドと同様のSQLを発行する -
length
メソッド: オブジェクトがメモリにロードされていない場合はレコードをメモリにロードしてからカウントを行う。すでにロードされている場合は新たなSQLを発行せずメモリ上のオブジェクトの数を返す
実際に発行されるSQLを見た方がイメージしやすいかもしれません。
count
メソッド:
-- COUNT(*)を発行しデータベース側でカウントする
SELECT COUNT(*) FROM table_name WHERE conditions
size
メソッド:
-- オブジェクトがメモリにロードされている場合はSQLを発行せずにメモリ上のオブジェクト数を返す
-- オブジェクトがメモリにロードされていない場合は`count`メソッドと同じSQLを発行
SELECT COUNT(*) FROM table_name WHERE conditions
length
メソッド:
-- オブジェクトがメモリにロードされている場合はSQLを発行せずにメモリ上のオブジェクト数を返す
-- オブジェクトがメモリにロードされていない場合は全レコードを取得し、メモリにロードしてからカウントする
SELECT * FROM table_name WHERE conditions
count, size, lengthの使い分け
これらのメソッドの挙動の違いを理解したところで、それぞれのメソッドをどのように使い分けるべきかを考えてみます。
大まかに言えば次のような判断基準でよいと思いました。
- データベース上のレコード数のみが必要で、オブジェクトの内容は不要 →
count
メソッド - 後続の処理でActive Recordオブジェクトの配列を使用する →
length
メソッド - 幅広いケースに柔軟に対応したい →
size
メソッド
size
メソッドはオブジェクトの取得状況に応じた柔軟な対応をしてくれるため、多くの場面で効率的に動作すると思われます。
次にそれぞれの使い分けを具体的なコードでも確認します。
count
メソッドの使用例
下記の例ではPost
オブジェクトの件数を取得後、後続の処理でPost
オブジェクトの配列であるposts
を使っていません。
このように件数だけが必要でオブジェクトの内容が不要な場合、count
メソッドが効率的です。
大量のレコードがある場合でも、データベース側でカウントを行うため、メモリ効率の点で優れています。
posts = Post.all
post_count = posts.count # SELECT COUNT(*) FROM "posts" が発行
puts "投稿数: #{post_count}"
size
メソッドの使用例
下記の例だとposts.each
の時点でオブジェクトがメモリにロードされています。
そのためposts.size
とすることで、SQLが再び発行されることを防げます。
これはposts.length
にした場合も同様です。
posts = Post.all
posts.each { |post| puts post.title } # SELECT "posts".* FROM "posts" が発行
post_count = posts.size # posts.eachでオブジェクトがロードされているため、SQLは発行されない
逆にposts.count
としてしまうと、posts.each
でオブジェクトを取得しているにも関わらず、件数を取得するためのSQLが再度発行されてしまうため非効率です。
posts = Post.all
posts.each { |post| puts post.title } # SELECT "posts".* FROM "posts" が発行
post_count = posts.count # SELECT COUNT(*) FROM "posts" が発行されてしまう
length
メソッドの使用例
下記の例ではposts.length
の直後のposts.each
で各post
オブジェクトが利用されます。
そのため事前にposts.length
でロードしておけばposts.each
ではそのロードされたオブジェクトを利用可能です。
posts = Post.all
post_count = posts.length # SELECT "posts".* FROM "posts" を発行
posts.each { |post| puts post.title } # すでにロードされているため、新たなSQLは発行されない
このケースでlength
の代わりにposts.count
やposts.size
とするのは非効率です。
count
とsize
は件数のみを取得するため、posts.each
でオブジェクトの内容を取得するSQLを再度発行しなくてはならないからです。
posts = Post.all
post_count = posts.count # SELECT COUNT(*) FROM "posts" を発行し、件数だけを取得
posts.each { |post| puts post.title } # SELECT "posts".* FROM "posts" を発行しないといけない
後続の処理でオブジェクトの内容も必要になるのであれば、最初からposts.length
を使っておくのが効率的です。
ただし、大量のレコードがある場合にlength
メソッドを使ってしまうとパフォーマンス上の問題を引き起こしかねません。
上記のケースでposts
が100万件あったと仮定すると、posts.length
の実行で100万件のPost
オブジェクトがメモリに作成されてしまい、メモリ使用量が大きくなってしまうからです。
こういった場合はcount
メソッドを使ってデータベース側でカウントを行うか、limit
を使用してロードするレコード数を制限することでメモリ使用量を抑えることができます。
# 100万件のレコードがあるとします
posts = Post.all
post_count = posts.length # このメソッド呼び出しで100万件のレコードがメモリにロードされます
# 改善例1: countメソッドを使用
posts = Post.all
post_count = posts.count # データベース側でカウント
# 改善例2: limitを使用(一部のレコードだけが必要な場合)
posts = Post.limit(20)
posts.length # 最大20件のみメモリにロード
注意: size, lengthは最新の状態を反映しないことがある
size
メソッドやlength
メソッドは最新のデータベースの状態を反映しないことがあります。
オブジェクトがすでにメモリにロードされている場合、size
とlength
は新しいSQLを発行しないからです。
実際のコードで説明します。
rails-app> posts.size
Post Count (0.3ms) SELECT COUNT(*) FROM "posts"
=> 8
rails-app> posts.length
Post Load (0.7ms) SELECT "posts".* FROM "posts"
=> 8
rails-app> posts.count
Post Count (0.4ms) SELECT COUNT(*) FROM "posts"
=> 8
rails-app> Post.create(content: "New Content")
TRANSACTION (0.1ms) begin transaction
Post Create (2.2ms) INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id" [["content", "New Content"], ["created_at", "2024-08-10 01:10:37.192066"], ["updated_at", "2024-08-10 01:10:37.192066"]]
TRANSACTION (0.8ms) commit transaction
=> #<Post:0x000000012d71aa10 id: 9, content: "New Content", created_at: "2024-08-10 01:10:37.192066000 +0000", updated_at: "2024-08-10 01:10:37.192066000 +0000">
rails-app> posts.size
=> 8
rails-app> posts.length
=> 8
rails-app> posts.count
Post Count (4.6ms) SELECT COUNT(*) FROM "posts"
=> 9
rails-app> posts.size
=> 8
rails-app> posts.length
=> 8
Post.create
で新しいレコードを作成した後も、size
メソッドとlength
メソッドはSQLを発行せず、同じ値を返しています。
すでにメモリにロードされているオブジェクトを参照しているからです。
一方でcount
メソッドはSELECT COUNT(*) FROM "posts"
で最新の件数を取得しています。
このように、size
メソッドとlength
メソッドは必ずしも最新のデータベースの状態を反映するわけではないため、注意しましょう。
おわりに
最後におさらいです。
-
count
メソッド:COUNT(*)
のSQLを発行し、データベース側でカウントを実行する -
size
メソッド: オブジェクトがメモリにロードされているならSQLを発行せずにメモリ上のオブジェクトの数を返す。ロードされていない場合はcount
メソッドと同様のSQLを発行する -
length
メソッド: オブジェクトがメモリにロードされていない場合はレコードをメモリにロードしてからカウントを行う。すでにロードされている場合は新たなSQLを発行せずメモリ上のオブジェクトの数を返す
一見ただのエイリアスメソッドに思えますが、内部的な挙動はだいぶ異なるため意識して使い分けましょう。
また記事に関して誤りがありましたらご指摘いただけますと幸いです。
参考資料