【Rails】世界で一番分りやすく詳しいcountメソッドの使い方

Rails

モデル名#countメソッドとは

各モデル(テーブル)のレコード数を獲得するメソッドです。

countメソッドの使い方
リンクをコピーしました

countメソッドはテーブルのレコード数がいくつあるか確認するときによく使うメソッドです。確認の用途以外にもレコードの数を表示したい場合にもよく使います。

サンプルコード
リンクをコピーしました

ターミナル
1
2
3
4
User.count

SELECT COUNT(*) FROM `users`
=> 7

User(モデル名).countと書くことで該当のテーブルのレコードの数を取得できます。今回のサンプルコードの場合だとusersテーブルのレコードは7つあるという意味になります。

テーブルのレコードの数を数えられるcountメソッドがどの様な場面で使用できるか見ていきましょう!

ある特定の条件でレコードを数えたい場合
リンクをコピーしました

ただテーブルにある全てのレコードの数を数えるだけではなくて、「この条件の場合だとレコードはいくつあるんだろう?」みたいなときにもcountメソッドはよく使います。pikawakaの公開済み記事を数える例で見ていきましょう。

ターミナル
1
2
3
Blog.where(status: 'publish').count
SELECT COUNT(*) FROM `blogs` WHERE `blogs`.`status` = 2
=> 80

↑はどの様なレコードの数を数えたかというと「今公開されている記事の本数はいくつあるのだろう?」と知るために今公開されている記事の本数を数えました。今回の様に特定の条件で該当するレコードがいくつあるか調べる用途でもcountメソッドはよく使います。

グループごとのレコードを数えたい場合
リンクをコピーしました

テーブルにある全てのレコードを数えるだけではなくて、特定のカラムに対して特定の値が入ってあるレコード数を取得したいというケースがあります。分りやすく言うとsexカラム(性別)の値が男性、女性のレコードはそれぞれいくつあるんだろう?とそれぞれのレコード数をグルーピング(まとめたい)するときに使います。

その場合groupメソッドとcountメソッドを一緒に使うと性別ごとのレコード数を取得できます。

ターミナル
1
2
3
User.group(:sex).count
  SELECT COUNT(*) AS count_all, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`
=> {"女性"=>3, "男性"=>4}

{"グルーピングした値"(男性,女性) => "グルーピングした値のレコード数"(男性,女性のレコード数)}
のhashを返り値として受け取ることができ、このhashから特定の値(男性,女性)がそれぞれいくつ入ってるか知ることが出来ます。

groupメソッドとcountメソッドを一緒に使う箇所は下記の2点あります。

  1. 男性,女性のグルーピングの様な特定の値のレコードがいくつあるか知りたい場合
  2. グルーピングしたモノを数の多い順に並び替えてランキング化したい場合

1については上記で解説したので、次はランキング化したい場合はどの様な場合に使えるか見ていきましょう!

ランキング機能の作り方
リンクをコピーしました

2 グルーピングしたモノを数の多い順に並び替えてランキング化したい場合について説明します。
ランキングのイメージをし易い様に、pikawakaのそれぞれのカテゴリーのブログ数ランキングを作ってみましょう!

まず初めに{"グルーピングした値"=> "グルーピングした値のレコード数"}でまとめます。

サンプルコード(groupメソッドとcountメソッドとの組み合わせ)
リンクをコピーしました

ターミナル
1
2
3
4
5
6
7
8
9
Blog.group(:category_id).count
 SELECT 
  COUNT(*) AS count_all, `blogs`.`category_id` AS blogs_category_id
 FROM 
  `blogs` 
GROUP BY 
  `blogs`.`category_id`

=> {2=>30, 12=>23, 22=>13, 42=>10, 52=>6, 62=>7}

今回はcategory_idでまとめているので、返り値のhashの意味はcategory_idが2のレコードは30個あります。category_idが12のレコードは23個あります。このように存在するcategory_idのレコードがいくつあるかcountで数え、{category_id => 存在したcategory_idのレコード数}で返しています。

それではどうすればランキング機能を作れるのでしょうか?
先程のサンプルコードの返り値を振り返ってみましょう。

ruby
1
2
3
# サンプルコードの返り値

{2=>30, 12=>23, 22=>13, 42=>10, 52=>6, 62=>7}

この返り値のhashは{category_id => 存在したcategory_idのレコード数}の意味だとお伝えしました。このレコード数はblogsテーブルのレコード数という意味なので、それぞれのカテゴリーの記事の本数がこのhashから分かります。

つまりカテゴリーの記事の本数ランキングを作成できます。

joinsメソッドとorderメソッドを使えば、カテゴリーの記事名、記事の本数のhashを作成できます。
joinsメソッドはテーブルを繋げることができ、orderメソッドは順番を並び替えることが出来ます。今回はblogsテーブルとcategoriesテーブルを繋げて

{"categoriesテーブルのnameカラム(記事名)"=> "記事の本数"}でまとめます。

ターミナル
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Blog.joins(:category).group("categories.name").order('count_all DESC').count
SELECT 
  COUNT(*)  AS count_all, categories.name AS categories_name 
FROM 
  `blogs` 
INNER JOIN 
  `categories` 
ON 
  `categories`.`id` = `blogs`.`category_id` 
GROUP BY 
  categories.name 
ORDER BY 
  count_all DESC
=> {"Ruby"=>31, "Rails"=>30, "IT用語"=>13, "JavaScript"=>9, "お役立ち情報"=>7, "HTML5・CSS3"=>6}

こうするとランキング機能を作れるイメージを持てたのではないでしょうか。
では実際にアプリケーションでどの様に使えるか見ていきましょう。

アプリケーションでランキング機能を作ってみる
リンクをコピーしました

コントローラーで先程書いた{カテゴリー名 => カテゴリーごとの記事本数}を取得して@blogsのインスタンス変数に代入します。

blogs_controller.rb
1
2
3
  def index
    @blogs = Blog.joins(:category).group("categories.name").order('count_all DESC').count
  end

ランキングデザインにするために簡単にtableのcssを当てました。

ランキング画像

index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
  table, th, td {
    border-collapse: collapse;
    border: 1px solid #ddd;
  }

  td {
    text-align: center;
  }
</style>

<table>
  <thead>
    <tr>
      <th>順位</th>
      <th>カテゴリー</th>
      <th>記事の本数</th>
    </tr>
  </thead>
  <tbody>
    <% @blogs.each_with_index do |blog, i| %>
      <tr>
        <td><%= i + 1 %></td>
        <td><%=blog.first %></td>
        <td><%=blog.second %></td>
      </tr>
    <% end %>
  </tbody>
</table>

each_with_indexはeachで回したときにそれぞれの要素が何番目か返してくれるメソッドです。
each_with_indexで回したblogはそれぞれのhashの要素が配列で入っているため、カテゴリー名を1つ目の配列の要素first、カテゴリーごとの記事の本数を二つ目の要素secondで返しています。

blogsの中身
1
2
3
4
5
p @blogs
=> {"Ruby"=>31, "Rails"=>30, "IT用語"=>13, "JavaScript"=>9, "お役立ち情報"=>7, "HTML5・CSS3"=>6}

 p @blogs.first
=> ["Ruby", 31]

基本的にランキングを作成するときはgroupとcountを一緒に使って実装するので、ランキング機能を作成したい人はぜひ上記を参考に実装して貰えればと思います。

モデルのcountメソッドと配列のcountメソッドとの違いについて
リンクをコピーしました

Railsでデータベースのテーブルのレコード数を数えるモデル名.countメソッドについて解説してきましたが、ここからはテーブルのレコードを数えるcountメソッドではなくて配列の要素を数えるcountメソッドについて解説していきます。

railsには似ている名前のメソッドや実際の動きもほとんど一緒だが微妙に動きが違うというメソッドが多くあるので、似ているメソッドのそれぞれの挙動の違いを理解しましょう!

配列のcountメソッドとの違い
リンクをコピーしました

配列のcountメソッド
1
2
 [0, 0, 1,2,3].count
=> 5

[0, 0, 1,2,3]の要素は全部で5つありますね。その全ての要素をcountした数を返しているメソッドになります。では、引数を与えた場合はどうなるのでしょうか?

引数を与えた場合

count(数字)
1
2
3
4
5
6
7
8
 [0, 0, 1,2,3].count(0)
=> 2

[0, 0, 1,2,3].count(1)
=> 1

 [0, 0, 1,2,3].count(4)
=> 0

こちらを見てもらって分かる通り、引数に与えた値がその配列にいくつあるか数えるメソッドになります。0の場合は2だし、1の場合は1つだし4は存在しないので0が返ってきます。もちろん引数が文字列に対しても使えます。

引数が文字列の場合

count(文字列)
1
2
 ['田中', '田中', '田中',2,3].count('田中')
=> 3

配列の中に田中は3つあるので、3と返ってきます。このように特定の要素を数えたり、さらに便利なのがロジックを用いて要素を数えることが出来ます。例題を見ていきましょう。

ロジックを使ったcountメソッド

count{ロジック}
1
2
[100, 90, 70, 85, 40, 60].count {|score| score > 80 }
=> 3

例えば生徒のtestのscoreを格納する配列があったとして、それぞれの要素で80点を超えたテストがいくつあるか調べるみたいな用途で、上記のようにロジックを用いてその要素を数える事もできます。今回の場合は80点を超えた要素は85, 90, 100の3つなので3と返ってきます。

stringのcountメソッドとの違い
リンクをコピーしました

配列のcountメソッドと違って文字列に対してのcountメソッドもあります。
配列の場合は配列の要素の数を数えるcountメソッドでしたが、文字列の中の特定の文字を数える用途でもcountメソッドを使用できます。

文字列のcountメソッド

文字列.count(文字列)
1
2
3
4
5
'pikawaka'.count('a')
=> 3

'pikawaka'.count('pa')
=> 4

1つ目の例ではaという文字がpikawakaの中でいくつあるか数えています。
2つ目の例ではpaという文字列を数えているのではなくて、p,aそれぞれの文字を数えた合計を返しています。pとaの合計文字数は4になるので4を返しています。

sizeメソッドとの違い
リンクをコピーしました

sizeメソッドも配列の要素の数を数えたり文字列の数を数えたり出来ます。ただcountメソッドとの違いは、特定の文字や要素を数えることは出来ません。例題を見ていきましょう。

sizeの使い方(配列)
1
2
3
4
5
 [0, 0, 1,2,3].size
=> 5

[0, 0, 1,2,3].size(0)
ArgumentError: wrong number of arguments (given 1, expected 0)

ただ配列の要素を数えるだけならばエラーは出ませんが、特定の要素を数える場合だとerrorが出ましたね。文字列に対してはどうか見ていきましょう。

sizeの使い方(文字列)
1
2
3
4
5
'pikawaka'.size
=> 8

'pikawaka'.size('a')
ArgumentError: wrong number of arguments (given 1, expected 0)

文字列に対しても全ての要素を数えることは出来ましたが、aという文字の要素を数えることは出来ません。ただcountメソッドも配列の要素の全ての数ならば数えることはできますが、文字列の全ての数は数えることは出来ません。

文字列にcountを使った例
1
2
'pikawaka'.count
ArgumentError: wrong number of arguments (given 0, expected 1+)

以上がcountメソッドとsizeメソッドの違いですね。特定の要素を数えたいのであればcountメソッドを使い、配列の全ての要素や全ての文字列を数えたいならばsizeメソッドを使いましょう。

lengthメソッドとの違い
リンクをコピーしました

lengthメソッドに関してはsizeメソッドのalias(エイリアス)なので、上記で説明したsizeメソッドと同じ動きになりますね。

lengthメソッドの使い方
1
2
3
4
5
'pikawaka'.length
=> 8

'pikawaka'.length('a')
ArgumentError: wrong number of arguments (given 1, expected 0)

sizeメソッドとlengthメソッドの違いはないので、どちらを使っても大丈夫です。今回のことで注意すべきなのは繰り返しになりますが、配列の特定の要素の数を数えたいとき、文字列の特定の要素を数えたいときはcountメソッドを使い、配列の全ての要素や全ての文字列を数えたいならばsizeメソッドかlengthメソッドを使い分けるようにしましょう!

モデルのレコードを数える際にパフォーマンスの良いcountメソッドの使い分け
リンクをコピーしました

最後はアソシエーション先のレコードをカウントすることにおいて、よりパフォーマンスが良くなる方法について解説します。アソシエーション先のレコードをカウントする際にパフォーマンスを良くする方法は2つあります。

  1. railsのアソシエーションのオプションであるcounter_cacheを使う方法

  2. sizeメソッドを使って既に読み込んでいる場合はSQLを読み込まずにレコードの数を返す方法

まず1のアソシエーションのオプションであるcounter_cacheを使う方法について解説します。

counter_cacheを使ってパフォーマンスを向上させる方法
リンクをコピーしました

couter_cacheとは、関連するテーブルのレコード数をキャッシュで持っておけるRailsの機能です。

キャッシュで持っているとわざわざsqlでデータを取りに行かなくてもキャッシュで関連先のレコード数を返すことが出来るので、sqlでデータを取るよりもスピードが圧倒的に向上します。

pikawakaにはいいね機能はありませんが、いいね機能があると仮定してpikawakaを例に一つ一つの記事のいいねのカウントをcounter_cacheで実装します。

blog.rb
1
  has_many :likes, dependent: :destroy
like.rb
1
  belongs_to :blog, counter_cache: :likes_count
マイグレーションファイル
1
2
3
4
5
class マイグレーション名 < ActiveRecord::Migration[5.2]
  def change
      add_column :blogs, :likes_count, :integer
  end
end

like.rbにbelongs_to :blog, counter_cache: :likes_countと定義することによって、記事をいいねするたびにblogsテーブルのblogs_countも増え、blogs_countをキャッシュで持っておくことが出来ます。

この定義により、 blog.likes.sizeとしてもsqlを発行せずにblogのいいねされている数をキャッシュから受け取ることが出来るので、スピードがかなり早くなります。

ターミナル
1
2
blog.likes.size
=> 2

いいねボタンを押した場合

ターミナル
1
2
  SQL (0.4ms)  INSERT INTO `likes` (`user_id`, `blog_id`, `created_at`, `updated_at`) VALUES (4, 4, '2019-08-12 07:23:22', '2019-08-12 07:23:22')
  SQL (1.8ms)  UPDATE `blogs` SET `likes_count` = COALESCE(`likes_count`, 0) + 1 WHERE `blogs`.`id` = 4

blog_idが4のlikeのレコードが増えているのと、blogsテーブルのidが4のlikes_countの数が増えています。このようにしていいねされるたびにblogsのlikes_countは一つずつ増えていきます。逆にいいねを解除した場合はどうなるか見ていきましょう。

ターミナル
1
2
  SQL (1.4ms)  DELETE FROM `likes` WHERE `likes`.`id` = 4
  SQL (0.2ms)  UPDATE `blogs` SET `likes_count` = COALESCE(`likes_count`, 0) - 1 WHERE `blogs`.`id` = 4

上記のように該当のlikesレコードは削除され、blogsテーブルのlikes_countも一つ減ります。
では、likes_countカラムを増やしてcounter_cacheを実装することによって、どの様にパフォーマンスが向上していくか見てみましょう。

blogからアソシエーション先のいいねのカウントのパフォーマンスをcountメソッドを使うかlikes_countを使うかで比べていきます。

ターミナル
1
2
3
4
5
6
7
8
9
10
11
12
Blog.first(4).map(likes_count)
Blogs Load (0.3ms)  SELECT  `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` ASC LIMIT 4
=> [2, 1, 1, 1]

Blog.first(4).map{|blog| blog.likes.count}

  Blog Load (0.3ms)  SELECT  `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` ASC LIMIT 4
   (0.2ms)  SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 1
   (0.2ms)  SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 2
   (0.2ms)  SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 3
   (0.2ms)  SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 4
=> [2, 1, 1, 1]

上記を見るとlikes_countを使ってる場合はSQLでデータを読み込みに行っておりません。それに比べてlikes.countとしている場合は、毎回SQL文を発行してcountの数を読み込みに行っています。

これが数万レコードにもなるとかなりパフォーマンスも変わってくるのが予想できます。countメソッドを使用すると毎回SQLを読み込むことになるので、パフォーマンスを考えるとcounter_cacheを使ったほうが良いです。

sizeメソッドを使ってパフォーマンスを向上させる方法
リンクをコピーしました

sizeメソッドを使って既に読み込んでいる場合はSQLを読み込まずにレコードの数を返す方法について解説します。この方法を使うとcounter_cacheを使用せずに処理を早くすることが出来ます。

counter_cacheを実装していなかったらlikes_countを使って実装するのはデータ上の関係もあって面倒くさくなる部分もあると思うので、その場合はsizeメソッドを使ってパフォーマンス向上を図りましょう。

sizeメソッドを使うとなぜSQLを読み込まないか理由を説明するには、loaded?メソッドの説明が必要になってくるのでloaded?メソッドについて解説していきます。

loaded?メソッドとはアソシエーション先のレコードがすでにロードされているか確かめるメソッドです。

ターミナル
1
2
3
4
5
6
7
8
9
10
blog = Blog.first
blog.likes.loaded?
=> false
blog.likes.count
SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 1
=> 2

blog.likes.size
SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 1
=> 2

loaded?メソッドはアソシエーション先のレコードを読み込んでいるか確認するメソッドです。今回loaded?としてもfalseが返ってくるので、まだblog_idが1のアソシエーション先のlikesは読み込んでいないという意味になります。

その場合は、blog.likes.countとしてもblog.likes.sizeとしてもSQLをとってきます。ただし一度アソシエーション先を読み込むとsizeメソッドの場合はSQLを読み込みにいきません。countメソッドはloadされていても毎度SQLを読み込みに行きます。

countメソッドとsizeメソッドの違い

ターミナル
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
blog = Blog.first
blog.likes.loaded?
=> false

blog.likes
=> [#<Like:0x007f98de47b480 id: 4, user_id: 4, blog_id: 1, created_at: Mon, 12 Aug 2019 16:49:33 JST +09:00, updated_at: Mon, 12 Aug 2019 16:49:33 JST +09:00>,
 #<Like:0x007f98e0cec590 id: 7, user_id: 5, blog_id: 1, created_at: Mon, 12 Aug 2019 16:56:17 JST +09:00, updated_at: Mon, 12 Aug 2019 16:56:17 JST +09:00>]

blog.likes.loaded?
=> true

blog.likes.count
SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 1
=> 2

blog.likes.count
SELECT COUNT(*) FROM `likes` WHERE `likes`.`blog_id` = 1
=> 2

blog.likes.size
=> 2
blog.likes.size
=> 2

上記のようにloaded?メソッドで一度アソシエーション先を読み込んでいるならばsizeメソッドの方は既に読み込んでいるデータを使うので、SQLを読み込みにいくより圧倒的に早いです。その点countメソッドの場合はloadしていても毎度SQLを読み込みに行くので、これが数万件になってくると明らかにパフォーマンスに差が出てきます。

なのでcounter_cacheを実装していない場合はアソシエーション先のデータを読む場合はsizeメソッドを使いましょう。

まとめ

  • countメソッドとはレコードの数を数えるメソッドです。
  • groupメソッドと一緒に使うとランキング機能を作成できます。
  • アソシエーション先のレコードをcountする場合はcounter_cache・sizeメソッドを使うとパフォーマンスが向上します。