1. はじめに
パフォーマンス向上を考える時、count
よりsize
を使おうという話はよく耳にする。
「size
だとクエリを毎回発行しないから良いんでしょ」くらいのことは知っていても、実際それぞれのメソッドがどんな挙動をしていて、どう使い分けたら良いかあまり意識せずなんとなくで使っている人は多いのではないだろうか
この記事では実際に3つのメソッドの動きを確認し、「なぜcount
よりもsize
を使うべき」と言われているのか、また同時にどういう場合にどのメソッドを使ったら良いのかを考えてみたい
2. countメソッド
- SQLのCOUNTを使う
- キャッシュの有無に関係なく、毎回クエリを発行する
GitHub
irb(main):003:0> articles = Article.all
Article Load (0.2ms) SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...
# 1回目 クエリが発行される
irb(main):006:0> articles.count
(0.1ms) SELECT COUNT(*) FROM "articles"
=> 2
# 2回目 クエリが発行される
irb(main):007:0> articles.count
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 2
# データをメモリに保存(キャッシュ)
irb(main):008:0> articles.load
Article Load (0.2ms) SELECT "articles".* FROM "articles"
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-12-24 14:15:21.338593000 +0000">, #<Article id: 2, title: "Test", text: "fghijk", created_at: "2022-12-24 14:15:39.499115000 +0000", updated_at: "2022-12-24 14:15:39.499115000 +0000">]>
# 3回目 クエリが発行される
irb(main):009:0> articles.count
(0.1ms) SELECT COUNT(*) FROM "articles"
=> 2
キャッシュがあってもそれを使わず、クエリが発行されていることが分かる
# 呼びだせる
irb(main):024:0> Article.count
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 2
# NoMethodErrorとなる
irb(main):025:0> Article.length
Traceback (most recent call last):
1: from (irb):25
NoMethodError (undefined method `length' for #<Class:0x00007fa685c23340>)
# NoMethodErrorとなる
irb(main):026:0> Article.size
Traceback (most recent call last):
2: from (irb):25
1: from (irb):26:in `rescue in irb_binding'
NoMethodError (undefined method `size' for #<Class:0x00007fa685c23340>)
唯一モデルから呼び出すことができるのもcount
の特徴である。他の2つから呼び出すとエラーになる
3. sizeメソッド
・SQLのCOUNTを使う
・キャッシュがあれば使う
・実行してもメモリ上にデータを乗せることはできない
-> length
やload
等を使ってキャッシュを残す必要がある
GitHub
irb(main):001:0> articles = Article.all
(1.2ms) SELECT sqlite_version(*)
Article Load (0.1ms) SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...
# 1回目 クエリが発行される
irb(main):002:0> articles.size
(0.1ms) SELECT COUNT(*) FROM "articles"
=> 2
# 2回目 クエリが発行される
irb(main):003:0> articles.size
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 2
size
実行時にはキャッシュの保存がされないため、2回目実行した際は再度クエリが発行される
# キャッシュを残す
irb(main):004:0> articles.load
Article Load (0.2ms) SELECT "articles".* FROM "articles"
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-12-24 14:15:21.338593000 +0000">, #<Article id: 2, title: "Test", text: "fghijk", created_at: "2022-12-24 14:15:39.499115000 +0000", updated_at: "2022-12-24 14:15:39.499115000 +0000">]>
# 3回目 キャッシュを使うのでクエリが発行されない
irb(main):005:0> articles.size
=> 2
load
でキャッシュを残すと、size
を実行した時にキャッシュが使われるためクエリが発行されていないことが分かる
4. lengthメソッド
- SQLの実行結果の行数をカウントする
- キャッシュがあれば使う
- 実行時にメモリにキャッシュが保存される
GitHub
irb(main):001:0> articles = Article.all
(0.9ms) SELECT sqlite_version(*)
Article Load (0.1ms) SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...
# 1回目 length実行時に、キャッシュが保存される
irb(main):002:0> articles.length
Article Load (0.2ms) SELECT "articles".* FROM "articles"
=> 2
# 2回目 キャッシュを使う
irb(main):003:0> articles.length
=> 2
length
の場合、実行時にキャッシュが保存されるため、2回目実行時にはそのキャッシュが使われていることが分かる
5. 注意しておきたいこと
キャッシュを使うということは、必ずしもそのデータが最新状態とは限らない
irb(main):002:0> articles = Article.all
Article Load (0.1ms) SELECT "articles".* FROM "articles" /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Test", text: "abcde", created_at: "2022-12-24 14:15:21.338593000 +0000", updated_at: "2022-1...
irb(main):003:0> articles.count
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 2
irb(main):004:0> articles.size
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 2
irb(main):005:0> articles.length
Article Load (0.2ms) SELECT "articles".* FROM "articles"
=> 2
# 新しいデータを追加
irb(main):006:0> Article.create(title: 'Test', text: 'hogehoge')
TRANSACTION (0.1ms) begin transaction
Article Create (0.6ms) INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "Test"], ["text", "hogehoge"], ["created_at", "2022-12-24 15:40:41.200775"], ["updated_at", "2022-12-24 15:40:41.200775"]]
TRANSACTION (1.4ms) commit transaction
=> #<Article id: 3, title: "Test", text: "hogehoge", created_at: "2022-12-24 15:40:41.200775000 +0000", updated_at: "2022-12-24 15:40:41.200775000 +0000">
# クエリを実行しているので、最新の件数を取得する
irb(main):007:0> articles.count
(0.2ms) SELECT COUNT(*) FROM "articles"
=> 3
# キャッシュを使うので、更新前の件数を取得する
irb(main):008:0> articles.size
=> 2
# キャッシュを使うので、更新前の件数を取得する
irb(main):009:0> articles.length
=> 2
上の例のように、新しいデータが追加・削除等された場合に、キャッシュを使うsize
やlength
だと最新状態の件数が取得できるとは限らない。思考停止でcount
よりsize
を優先的に使おうとすると、誤った件数を取得してしまうことがあるので、この点は注意が必要である
6. なぜcountやlengthよりもsizeを使うべきなのか?
countとsizeの対比
count
は毎回クエリが発行されるため、size
よりも処理が重くなりがちである。
(特に何万件とデータがある場合は、パフォーマンスの問題が生じることが多い)
lengthとsizeの対比
length
はSQLのCOUNTを使って件数を取得しているのではなく、実行結果の行数を数えてその件数を取得している。処理的にはSQLのCOUNTを使うcount
やsize
方が処理が軽い
つまりcountやlengthよりもsizeを使うべき理由は...
size
はcount
とlength
の良さを併せ持ち、3つの中では一番バランスが取れたメソッドであるから。よって、何か特別な理由がない限りsize
を使うのが良さそうという結論になる。
そうは言ってもこの話は"基本的には"が大前提なので、結論としては以下である
7.結論
- データが更新される可能性がない場合には、無駄なクエリ発行を抑えるために
size
を用いる - データが更新される可能性がある場合には、正しい件数を数えるために
count
を用いる -
length
はキャッシュの保存も一緒に実行すべき時に用いる(既にキャッシュが存在し、件数を数えるだけみたいな時やその後キャッシュを使う予定がない場合は使わない) - 上記の通り、どのような場合も
count
よりsize
を使うべきというわけではなく、その場に適したメソッドを取捨選択することが必要である
参考
https://mgre.co.jp/blog/3199
https://himakuro.com/rails-count-length-size-guide
https://ohbarye.hatenablog.jp/entry/2021/07/03/002817
https://medium.com/ruby-daily/comparing-count-length-and-size-in-ruby-on-rails-e061840bc19e