LoginSignup
10
5

More than 1 year has passed since last update.

【Rails】なぜcountよりもsizeを使うべきなのか?〜lengthの動きも一緒に確認〜

Last updated at Posted at 2022-12-27

1. はじめに

パフォーマンス向上を考える時、countよりsizeを使おうという話はよく耳にする。
sizeだとクエリを毎回発行しないから良いんでしょ」くらいのことは知っていても、実際それぞれのメソッドがどんな挙動をしていて、どう使い分けたら良いかあまり意識せずなんとなくで使っている人は多いのではないだろうか

この記事では実際に3つのメソッドの動きを確認し、「なぜcountよりもsizeを使うべき」と言われているのか、また同時にどういう場合にどのメソッドを使ったら良いのかを考えてみたい

2. countメソッド

  • SQLのCOUNTを使う
  • キャッシュの有無に関係なく、毎回クエリを発行する
    GitHub
console
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

キャッシュがあってもそれを使わず、クエリが発行されていることが分かる

console
# 呼びだせる
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を使う
・キャッシュがあれば使う
・実行してもメモリ上にデータを乗せることはできない
-> lengthload等を使ってキャッシュを残す必要がある
GitHub

console
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回目実行した際は再度クエリが発行される

console
# キャッシュを残す
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
console
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. 注意しておきたいこと

キャッシュを使うということは、必ずしもそのデータが最新状態とは限らない

console
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

上の例のように、新しいデータが追加・削除等された場合に、キャッシュを使うsizelengthだと最新状態の件数が取得できるとは限らない。思考停止でcountよりsizeを優先的に使おうとすると、誤った件数を取得してしまうことがあるので、この点は注意が必要である

6. なぜcountやlengthよりもsizeを使うべきなのか?

countとsizeの対比

countは毎回クエリが発行されるため、sizeよりも処理が重くなりがちである。
(特に何万件とデータがある場合は、パフォーマンスの問題が生じることが多い)

lengthとsizeの対比

lengthはSQLのCOUNTを使って件数を取得しているのではなく、実行結果の行数を数えてその件数を取得している。処理的にはSQLのCOUNTを使うcountsize方が処理が軽い

つまりcountやlengthよりもsizeを使うべき理由は...

sizecountlengthの良さを併せ持ち、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

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5