はじめに
以下の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を発行せずメモリ上のオブジェクトの数を返す
一見ただのエイリアスメソッドに思えますが、内部的な挙動はだいぶ異なるため意識して使い分けましょう。
また記事に関して誤りがありましたらご指摘いただけますと幸いです。
参考資料