すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

【Rails】 groupメソッドの使い方とは?仕組みを図解で丁寧に解説!

ぴっかちゃん
ぴっかちゃん

groupメソッドとは、指定したカラムのデータの種類ごとに、データをまとめることが出来るメソッドです。

例えば、以下のようにgroupメソッドを使用すると、指定したカラムのデータ(この場合は男性・女性)が重複している場合、レコードをまとめます。

groupメソッドの使用例
1
2
User.group(:sex)
SELECT `users`.* FROM `users` GROUP BY `users`.`sex`

まとめたレコードごとに一番小さいidが1件だけ表示されるので、男性・女性のレコードが複数あっても以下のような結果になります。

groupでは1番小さいidにまとめられる画像

表示はそれぞれ一件だけですがプログラムが実行されている裏側では、すべてのレコードが指定したカラムでまとめられています。

groupメソッドの基本的な使い方

この章では、groupメソッドの基本的な使い方について1つ1つ解説します。

サンプルコードでgroupメソッドを理解する

groupメソッドとは、指定したカラムのレコードの種類ごとにデータをまとめるメソッドです。下記のusersテーブルのsexカラムには登録しているユーザーの性別が保存してあります。

「それぞれの性別(男性・女性)ごとにレコードはいくつずつ入っているのだろう?」と思ったときにそれぞれの性別ごとのレコードをまとめられるのがgroupメソッドです。

groupメソッドは、具体例を持って説明するほうが分かり易いので、プログラマンファミリーを例にgroupメソッドを説明します。

以下はプログラマンファミリーのそれぞれのメンバーが載っているusersテーブルの情報です。

usersテーブル

id name age sex tall income(月収)
1 programan 25 男性 175 300000
2 programan_father 58 男性 175 500000
3 programan_mother 58 女性 162 100000
4 programan_bigsister 30 女性 158 180000
5 programan_sister 20 女性 154 80000
6 programan_bigbrother 28 男性 178 350000
7 programan_brother 22 男性 172 60000

この上記のテーブルからそれぞれ男性女性のレコードを取得します。

男性のレコード
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
User.where(sex: '男性')
=> [#<User:0x007fc281e32dd0
  id: 1,
  name: "programan",
  sex: "男性",
  age: 25,
  tall: 175,
  income: 300000>,
 #<User:0x007fc281e31688
  id: 2,
  name: "programan_father",
  sex: "男性",
  age: 58,
  tall: 175,
  income: 500000>,
 #<User:0x007fc281e31548
  id: 6,
  name: "programan_bigbrother",
  sex: "男性",
  age: 28,
  tall: 178,
  income: 350000>,
 #<User:0x007fc281e31408
  id: 7,
  name: "programan_brother",
  sex: "男性",
  age: 20,
  tall: 172,
  income: 60000>]

男性ユーザーのレコードは4つ存在します。

女性のレコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
User.where(sex: '女性')
=> [#<User:0x007fc281e7a4c8
  id: 3,
  name: "programan_mother",
  sex: "女性",
  age: 58,
  tall: 162,
  income: 100000>,
 #<User:0x007fc281e7a388
  id: 4,
  name: "programan_bigsister",
  sex: "女性",
  age: 30,
  tall: 158,
  income: 180000>,
 #<User:0x007fc281e7a248
  id: 5,
  name: "programan_sister",
  sex: "女性",
  age: 20,
  tall: 154
  income: 80000>]

女性ユーザーのレコードは3つ存在します。
男性・女性のレコードは合わせて7つ存在します。

この男性・女性のレコードたちをgroupメソッドでsexカラムをまとめると、sexカラムのデータごとにどの様にまとめられるか見ていきましょう。

groupメソッドを単体で使った場合

groupメソッドを単体で使った場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
User.group(:sex)
SELECT `users`.* FROM `users` GROUP BY `users`.`sex`
=> [#<User:0x007fc281fcae90
  id: 3,
  name: "programan_mother",
  sex: "女性",
  age: 58,
  tall: 162.0,
  income: 100000>,
 #<User:0x007fc281fcad50
  id: 1,
  name: "programan",
  sex: "男性",
  age: 25,
  tall: 175.0,
  income: 300000>]

groupメソッドは、指定したカラムのデータ(この場合は男性・女性)が重複している場合、レコードをまとめます。まとめたレコードごとに一番小さいidが1件だけ表示されるので、男性・女性のレコードが複数あっても上記のような結果になります。

groupでは1番小さいidにまとめられる画像

ここで重要なことは、表示は一件だけですがプログラムが実行されている裏側では、すべてのレコードが指定したカラムでまとめられていることです。

group byでデータをまとめている画像

そして、groupメソッドを単体で使うとSELECT users.* FROM users GROUP BY users.sexのようなSQLが発行され、男性・女性それぞれのもっとも小さいIDのレコードの全てのカラムのデータが取得されます。

全てのカラムのデータが取得される画像

ここまでがgroupメソッドを単体で使用した場合の説明になります。指定したカラムのレコードの種類ごとにまとめ、種類でまとめられたレコードの1番小さいIDのレコード1件の全カラムを取得するのがgroupメソッドを単体で使った場合です。

しかし、下記の理由からgroupメソッドを単体で使ってもほとんど意味がありません。

  1. groupメソッドで指定したカラムのデータをまとめても、まとめただけで終わっていてまとめたデータを元に集計していないから。

  2. groupでまとめる用途で、最も小さいIDのレコードの全カラム情報を必要とするケースがほとんどないため。

ですからgroupメソッドは単体で使うのではなく、countメソッドやsumメソッドなどの集計メソッドと併用して使います。

集計メソッドについて詳しくは後述しますが、簡単に言うと複数のデータからデータの数や合計値・平均値などを抽出するメソッドです。

この集計メソッドとgroupメソッドを併用すれば、指定したカラムのレコード(種類)ごとにレコードの数を表示できたり、レコード(種類)の平均値が簡単に表示できて、groupメソッドの便利さを実感できます。

groupメソッドと集計メソッドの併用に関しては、最も使用するケースの多いgroupとcountメソッドを併用して使用するケースを見てみましょう。

groupメソッドとcountメソッドを併用して使用した場合

groupメソッドとcountメソッドを併用する方法を説明する前に、まず簡単にcountメソッドの説明をします。
countメソッドとはテーブルのレコード数を数えられるメソッドです。
プログラマンファミリーのユーザー数を数える場合は

countメソッドの説明
1
2
User.count
=> 7

この様に記述します。これでプログラマンファミリーのユーザーのレコード数は7であることが分かります。このレコード数を数えられるcountメソッドとgroupメソッドを併用すると、指定したカラムのレコード(種類)ごとにレコードの数を表示できます。

groupメソッドとcountメソッドを併用した場合
1
2
3
4
User.group(:sex).count
SELECT COUNT(*) AS count_all, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`

=> {"女性"=>3, "男性"=>4}

groupメソッドを単体で使用した場合は全カラム情報が返ってきてましたが、groupとcountを併用した場合は返り値はhashの形で返ってきます。

返り値がhashで返ってくることが、併用した場合の大きな特徴です。
どの様なデータからhashを作り出してるか見てみましょう。

groupメソッドとhashの返り値の関係を表す画像

上記の画像で示している通り、groupで指定したカラムのレコードの種類がkeyになります。今回でいうとsexカラムのレコードの種類は男性・女性なのでそれぞれのレコードがkeyになります。

そしてvalueに入るものは、レコードの種類ごとにまとめられた重複したデータの集計結果になります。今回でいうと男性・女性の重複したレコードがまとめられ、男性・女性のレコード数がvalueに入ることになります。

グループごとにデータをまとめている表の画像

groupメソッドを単体で使った場合がなぜレコードの全カラム情報を返していたかというと、

groupメソッドを単体で使った場合 -->
1
SELECT `users`.* FROM `users` GROUP BY `users`.`sex`

SELECT users.*と全カラムを表示しているからです。
ただgroupとcountメソッドを併用して使用した場合は

groupメソッドとcountメソッドを併用した場合 -->
1
SELECT COUNT(*) AS count_all, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`

SELECT COUNT(*) AS count_all, users.sex AS users_sexと表示されているので、性別と性別ごとにカウントした数だけを表示してます。

つまり{"性別"=>性別ごとにカウントした数}のhashはSELECT COUNT(*) AS count_all, users.sex AS users_sexで抽出したデータがhashに入っている理屈になります。

ではgroupで指定したカラムが2つの場合はどうなるか見てみましょう。

groupメソッドにカラム2つを指定した場合 -->
1
2
3
User.group(:tall, :sex).count
SELECT COUNT(*) AS count_all, `users`.`tall` AS users_tall, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`tall`, `users`.`sex`
=> {[154, "女性"]=>1, [158, "女性"]=>1, [162, "女性"]=>1, [172, "男性"]=>1, [175, "男性"]=>2, [178, "男性"]=>1}

groupに複数のカラムを指定した場合はkeyが配列になります。

groupにはtallとsexを指定してるので、この2つのレコードのデータがペアになった状態でKeyが配列として格納されます。valueにはこの2つのレコードのペアが重複したレコードの数が入ることになります。

性別と身長がペアで重複したデータは175・男性のペアのみだったので、 [175, "男性"]のkeyにだけvalueは2になり、その他のレコードに関しては重複したデータはなかったので1件ずつになっているということです。

ただkeyが配列になってもhashが返り値として返るのは変わりません。

groupと集計メソッドを併用して使えば{"groupで指定したカラムのレコードの種類" => "まとめられたデータの結果"}のhashになることを覚えておきましょう。

groupメソッドの応用的な使い方

この章では、集計関数と併用して使う方法やランキング機能など応用的な使い方について解説します。

groupメソッドと集計関数を併用して使う方法

countメソッド以外にも、groupメソッドとよく併用する集計メソッドがあります。

集計メソッド

メソッド名 使い方 説明
sum モデル名#sum(:カラム名) 指定したカラムのレコードの合計値を表示
maximum モデル名#maximum 指定したカラムのレコードの最大値を表示
minimum モデル名#minimum 指定したカラムのレコードの最小値を表示
average モデル名#average 指定したカラムのレコードの平均値を表示

↑の集計メソッドとgroupメソッドを併用して、それぞれどの様なデータを取得できるか見ていきましょう。

カラムごとの合計値を表示する方法

まとめられたデータの合計値を表示するには、groupとsumメソッドを併用します。
プログラマンファミリーの男性・女性それぞれの月収を取得してみましょう。

サンプルコード

ターミナル -->
1
2
3
4
User.group(:sex).sum(:income)
SELECT SUM(`users`.`income`) AS sum_income,
 `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`
=> {"女性"=>360000, "男性"=>1210000}

男性・女性のレコードでまとめたincomeの合計値を取得しました。
group(:sex).sum(:income)でsexのレコードで重複しているincomeの合計値をsum(:income)で取得できます。

{"女性"=>sexカラムが女性のレコードのincomeの合計値, "男性"=>sexカラムが男性のレコードのincomeの合計値

性別ごとの月収の合計値の画像

gruopでまとめたカラムごとの最大値を表示する方法

ターミナル -->
1
2
3
User.group(:sex).maximum(:income)
SELECT MAX(`users`.`income`) AS maximum_income, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`
=> {"女性"=>180000, "男性"=>500000}

男性・女性のレコードでまとめたincomeの最大値を取得しました。
group(:sex).sum(:income)でsexのレコードで重複しているincomeの最大値をmaximum(:income)で取得できます。つまりプログラマンファミリーの男性・女性でそれぞれ1番高い月収のメンバーを抽出したということになります。

性別ごとの月収の最大値の画像

gruopでまとめたカラムごとの最小値を表示する方法

ターミナル -->
1
2
3
4
5
User.group(:sex).minimum(:income)

SELECT MIN(`users`.`income`) AS minimum_income, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`

=> {"女性"=>80000, "男性"=>60000}

男性・女性のレコードでまとめたincomeの最小値を取得しました。
group(:sex).minimum(:income)でsexのレコードで重複しているincomeの最小値をminimum(:income)で取得できます。つまりプログラマンファミリーの男性・女性でそれぞれ1番低い月収のメンバーを抽出したということになります。

性別ごとの月収の最小値の画像

groupでまとめたカラムごとの平均値を表示する方法

男性・女性のレコードでまとめたincomeの平均値を取得しました。
group(:sex).minimum(:income)でsexのレコードで重複しているincomeの平均値をaverage(:income)で取得できます。つまりプログラマンファミリーの男性・女性の平均月収を抽出したということになります。

性別ごとの月収の平均値の画像

平均値を表示してるので、男性・女性の平均月収に人数分かけると合計値と同じになります。

ターミナル -->
1
2
3
4
5
6
7
8
9
10
11
User.group(:sex).average(:income)
=> {"女性"=>120000, "男性"=>302500}

120000 * 3
=> 360000

302500 * 4
=> 1210000

User.group(:sex).sum(:income)
=> {"女性"=>360000, "男性"=>1210000}

平均値に人数分をかけたら合計値と同じなので、平均値を出せていることが分かります。

ランキング機能の作り方

groupメソッドを使ってランキング機能を作る方法を説明していきます。
例えば、プログランファミリーの中で背が高い人ランキングだったり年齢が高い人ランキングを表示してみます。
ランキングといってもいろいろあると思いますが、今回はプログランファミリーの中で「背が高い人ランキング」や「年齢が高い人ランキング」を表示していきます。

背が高い人ランキング -->
1
2
3
4
5
6
7
8
9
10
User.select(:id, :name, :tall).order('tall DESC')
SELECT `users`.`id`, `users`.`name`, `users`.`tall` FROM `users` ORDER BY tall DESC

=> [<User:0x007fc281af7800 id: 6, name: "programan_bigbrother", tall: 178>,
 <User:0x007fc281af7648 id: 1, name: "programan", tall: 175>,
 <User:0x007fc281af7490 id: 2, name: "programan_father", tall: 175>,
 <User:0x007fc281af7300 id: 7, name: "programan_brother", tall: 172>,
 <User:0x007fc281af7148 id: 3, name: "programan_mother", tall: 162>,
 <User:0x007fc281af6f90 id: 4, name: "programan_bigsister", tall: 158>,
 <User:0x007fc281af6e00 id: 5, name: "programan_sister", tall: 154>]
年齢が高い人ランキング -->
1
2
3
4
5
6
7
8
9
10
User.select(:id, :name, :age).order('age DESC')
SELECT `users`.`id`, `users`.`name`, `users`.`age` FROM `users` ORDER BY age DESC

=> [<User:0x007fc27c19b0e0 id: 2, name: "programan_father", age: 58>,
 <User:0x007fc27c19af28 id: 3, name: "programan_mother", age: 58>,
 <User:0x007fc27c19ad98 id: 4, name: "programan_bigsister", age: 30>,
 <User:0x007fc27c19abe0 id: 6, name: "programan_bigbrother", age: 28>,
 <User:0x007fc27c19aa28 id: 1, name: "programan", age: 25>,
 <User:0x007fc27c19a898 id: 5, name: "programan_sister", age: 20>,
 <User:0x007fc27c19a6e0 id: 7, name: "programan_brother", age: 20>]

この様にorderメソッドを使えば、簡単に順序を並び替えてランキング機能を作ることができます。
orderメソッドについては、詳しくはorderメソッドを徹底解説を参照して下さい。

今回の場合だと特にgroupメソッドは必要ありませんね。
ではどの様なケースでgroupメソッドを使ってランキング機能を作るのでしょうか?

それは主にgroupメソッドと集計メソッドを併用した場合のランキングになります。

集計メソッドは先ほど説明したsumやaverageやcountなどですね。
まずはgroupメソッドとcountメソッドを併用したモノを例に、どの様なランキングができるか説明していきます。

同い年の数ランキング -->
1
2
3
4
5
6
7
User.group(:age).order('count_age DESC').count(:age)

SELECT COUNT(`users`.`age`) AS count_age, `users`.`age` AS users_age
FROM `users` 
GROUP BY `users`.`age` ORDER BY count_age DESC

=> {58=>2, 25=>1, 30=>1, 20=>1, 28=>1, 22=>1}

このランキングはプログラマンファミリーの中で、同い年の人の数が多い順に数と年齢のセットで並べてます。

これだと一位は58歳、あとは全部同一2位ですね。

このようにgroupで集計した結果から数だったり、数値が大きい順に並び替えたりするときに、groupメソッドでランキング機能を作ることは良くあります。

ここで注目してほしいのがorder('count_age DESC')となっている部分です。もしこのorder('count_age DESC')がなかった場合、どうなるか見てみましょう。

orderがなかった場合 -->
1
2
3
 User.group(:age).count
SELECT COUNT(*) AS count_all, `users`.`age` AS users_age FROM `users` GROUP BY `users`.`age`
=> {20=>1, 22=>1, 25=>1, 28=>1, 30=>1, 58=>2}

この様に表示されて、数が一番多い58歳が先頭に来ておりません。理由は数が多い順にorderしていないからです。

order('count_age DESC')を使うと数が多い順に並び替えてくれます。ただこのcount_ageというのは、カラム名ではありません。なのになぜageではなくcount_ageにすると数が多い順に並び替えてくれるのでしょうか?

集計メソッド_カラム名の画像

まず上記の画像からcount_ageというものは、集計メソッド_カラム名からcount_ageであることがわかります。そしてこのcount_ageで出力している内容は同い年の年齢が重複された数です。hashのvalueに当たる部分ですね。

hashのvalueの画像

このcount_ageで出力して並んでいる順番は(1,1,1,1,1,2)になり、このままだと2を先頭に持ってくることができません。そこでorder('count_age DESC')とすることで降順になり(2,1,1,1,1,1)という順番で並べることができます。

order byで降順を説明している画像

この様に集計メソッド_カラム名の命名に対してorderで並び替えられることを知ると、groupごとに簡単に並び替えていろんなランキング機能を作ることができます。

仕組みは分かったと思うので、他に例も見ていきましょう

同じ身長の人の数が多い順(orderがなかった場合) -->
1
2
3
User.group(:tall).count(:tall)
SELECT COUNT(`users`.`tall`) AS count_tall, `users`.`tall` AS users_tall FROM `users` GROUP BY `users`.`tall`
=> {154=>1, 158=>1, 162=>1, 172=>1, 175=>2, 178=>1}

orderで指定しなかった場合は身長175が2つ重複しているのに先頭に来ませんね。

tall(orderがあった場合) -->
1
2
3
User.group(:tall).order('count_tall DESC').count(:tall)
SELECT COUNT(`users`.`tall`) AS count_tall, `users`.`tall` AS users_tall FROM `users` GROUP BY `users`.`tall` ORDER BY count_tall DESC
=> {175=>2, 162=>1, 158=>1, 154=>1, 178=>1, 172=>1}

orderをつけて同じ身長の人の数が多い順('count_tall DESC')と明示することによって、
175が先頭に来ました。

性別ごとの月収合計順(orderがなかった場合) -->
1
2
3
User.group(:sex).sum(:income)
SELECT SUM(`users`.`income`) AS sum_income, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex`
=> {"女性"=>360000, "男性"=>1210000}

男性ごとの月収合計の方が多いはずなのに女性の方が先に来てますね。
これも集計メソッド_カラム名通りにorder('sum_income DESC')とすると月収の合計値が高い順になります。

性別ごとの月収合計順(orderがなかった場合) -->
1
2
3
User.group(:sex).order('sum_income DESC').sum(:income)
SELECT SUM(`users`.`income`) AS sum_income, `users`.`sex` AS users_sex FROM `users` GROUP BY `users`.`sex` ORDER BY sum_income DESC
=> {"男性"=>1210000, "女性"=>360000}

この様に集計メソッド_カラム名にorderで順番を指定すれば、簡単にいろんなランキングが作れることが分かったと思います。

最後にorderが入った場合のsqlの細かい処理フローを見ていきましょう。

sqlのフローのイメージ -->
1
2
3
4
5
6
7
from(テーブル)
→ where(条件指定)
→ group(グルーピング)
→ having(グルーピングに条件指定)
→select(抽出)
→ order(抽出したデータの順番指定)
↑の手順全て踏んで作られたデータを抽出

GROUP BYごとに出した集計メソッド_カラム名のデータをSELECTで抽出して、その抽出したモノをORDERで順番通りに並び替えるという仕組みでgroupごとに集計したランキングを作ることができます。

whereとhavingの挙動については後述します。

sqlの話を出さないと仕組みを説明することが難しかったのでsqlの話を出しましたが、sql文を書く機会があったときにも上の話は参考になるので、ぜひ参考にして使いこなして下さい。

実際にgroupごとの数のランキングを出して、アプリケーションでどのように表示するかは、こちらのcountメソッドの記事に詳しく書いているので是非チェックしてください。

ランキング画像

ぴっかちゃん

今回のランキング機能のように、実務向けのややハイレベルなRuby on Railsの知識について学びたい方は、こちらの参考書が役に立つでしょう。Rails 6.0も含めて広い範囲を体系的に学ぶことができます。

gruopメソッドに条件を指定する方法(havingの使い方)

groupメソッドはレコードの種類ごとにデータをまとめられると説明しましたが、ある条件下でデータをまとめることも出来ます。その条件を指定するメソッドがhavingメソッドになります。
年齢が25歳以上の条件で、プログラマンファミリーの誰が該当するか見てみましょう

サンプルコード

ターミナル -->
1
2
3
4
5
User.group(:name, :age).having("age > 25").count

SELECT COUNT(*) AS count_all, `users`.`name` AS users_name, `users`.`age` AS users_age FROM `users` GROUP BY `users`.`name`, `users`.`age` HAVING (age > 25)

=> {["programan_bigbrother", 28]=>1, ["programan_bigsister", 30]=>1, ["programan_father", 58]=>1, ["programan_mother", 58]=>1}

group(:カラム名).having("条件式")と書くことで、条件式に当てはまったデータをまとめることができます。今回の場合でいうとプログラマンファミリーの中で25歳以上のメンバーだけを抽出した結果になります。

もう少し複雑にした条件もみてみましょう。

ターミナル -->
1
2
3
4
5
6
7
User.group(:sex, :tall).having("(sex = '男性' and tall > 174) or (sex = '女性' and tall > 155)").count

SELECT COUNT(*) AS count_all, `users`.`sex` AS users_sex, `users`.`tall` AS users_tall 
FROM `users` GROUP BY `users`.`sex`, `users`.`tall` 
HAVING ((sex = '男性' and tall > 174) or (sex = '女性' and tall > 155))

=> {["女性", 158]=>1, ["女性", 162]=>1, ["男性", 175]=>2, ["男性", 178]=>1}

今回の条件で言うと、男性が174・女性が155以上の身長という条件で抽出しました。この様にしてhavingを使えば特定の条件下でデータをまとめることができるので大変便利です。

whereメソッドとhavingメソッドの違い

whereメソッドとhavingメソッドは両方とも条件を指定して、データを抽出するメソッドです。
条件を指定すること自体は2つとも同じなのですが、大きく違う点は実行される順番です。
whereメソッドとhavingメソッドの挙動の順番を見比べていきましょう。

挙動の順番の違い

whereメソッドの場合 -->
1
2
3
4
5
User.group(:sex).where('income > 150000').count
SELECT COUNT(*) AS count_all, `users`.`sex` AS users_sex FROM `users` 
WHERE (income > 150000) GROUP BY `users`.`sex`

=> {"女性"=>1, "男性"=>3}

whereメソッドの場合だと、15万以上の月収があるメンバーだけに対して性別ごとにレコードを数えています。
その結果15万以上の月収があるメンバーは女性一人、男性三人です。

groupとwhereメソッドの画像

つまりwhereで抽出されたレコードに対して、groupでデータをまとめています。
それに対してhavingメソッドの挙動はwhereメソッドとどう違うのでしょうか?

havingメソッドの場合 -->
1
2
3
4
5
User.group(:sex).having('sum_income > 500000').sum(:income)
SELECT SUM(`users`.`income`) AS sum_income, `users`.`sex` AS users_sex FROM `users`
 GROUP BY `users`.`sex` HAVING (sum_income > 500000)

=> {"男性"=>1210000}

havingメソッドの場合は、groupでデータをまとめられた後に、そのまとめられたデータに対して条件を指定しています。

groupとhavingの画像

つまりwhereメソッドとhavingメソッドの違いは以下の2点です。

  1. whereメソッドはgroupメソッドが実行される前のデータに対して条件を指定。
  2. havingメソッドはgroupメソッドが実行された後に抽出されたデータに対して条件を指定。
フローのイメージ -->
1
2
where → group →  hash(whereで条件指定されたレコードに対してグルーピングしてハッシュ)
group → having → hash(グルーピングしたデータに条件指定してハッシュ)

この様なイメージです。もっと細かいsqlのイメージですと

sqlのフローのイメージ -->
1
2
3
4
5
6
7
from(テーブル)
→ where(条件指定)
→ group(グルーピング)
→ having(グルーピングに条件指定)
→select(抽出)
→ order(抽出したデータの順番指定)
↑の手順全て踏んで作られたデータを抽出

ですからそもそも今回のように150000以上の月収のメンバーに対してだけgroupでデータを絞り込みたい場合はwhereを使います!

groupで絞り込んだ後に抽出されたデータ(今回は性別ごとの月収の合計値)に対してデータを絞り込みたい場合はhavingを使うということです!

whereの場合だとgroupが実行される前に実行されるので、groupでまとめた合計値に対して条件を使おうとするとエラーが出ます。

groupを使われた後のデータに対してwhereを使った場合 -->
1
2
3
4
5
User.group(:sex).where('sum_income > 500000').sum(:income)

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'sum_income' in 'where clause':
SELECT SUM(`users`.`income`) AS sum_income, `users`.`sex` AS users_sex FROM `users` 
WHERE (sum_income > 500000) GROUP BY `users`.`sex`

このエラーが出る理由は、上記のsqlのsum_incomeをgroupでまとめて抽出する前に
whereでsum_incomeを指定しているので、sum_incomeみたいな値は知らないよとエラーが出ているということですね。(sqlのフローのイメージをみてもgroupよりも前にwhereは実行されている)

そういったgroupされた後のデータに対して条件で絞り込みたい場合は、havingを使わなければいけないということです。

whereとhavingの挙動の違いについては理解できたでしょうか?
下記の表にwhereとhavingの違いについてまとめたので
状況に応じてwhereとhavingを使い分けられるようにしておきましょう。

where having
groupの前に実行される groupの後に実行される
条件で絞り込まれたレコードに対して
データをまとめたい場合
groupでまとめられたデータに対して
条件を指定してデータを絞り込みたい場合

関連先テーブルのデータをgroupでまとめて表示する方法

最後には関連先テーブルで指定したカラムのレコードの種類をgroupでまとめて表示する、groupを応用した方法について説明します。
ここもプログラマンファミリーを例に見ていきましょう。

プログラマンファミリーのメンバーのjobsテーブルを作成します。

jobsテーブル

id name
1 プログラマン講師
2 家電メーカー
3 主婦
4 保育士
5 学生
6 営業

jobsテーブルのレコードと紐付けるためにusersテーブルにjob_idを加えます。

マイグレーションファイル -->
1
2
3
4
5
class マイグレーション名 < ActiveRecord::Migration[5.2]
  def change
      add_column :users, :job_id, :integer
  end
end

userとjobのアソシエーションを組みます。

user.rb -->
1
  belongs_to :job
job.rb -->
1
  has_many :users

これで下準備は終了です。

ここからjoinsメソッドとgroupメソッドを使って、usersテーブルの関連先であるjobsテーブルのnameカラムのレコードの種類でデータをまとめてみます。

joinsとgroupを使えば下記のサンプルコードのように、userがjobに紐付いているレコードで、データをまとめることができます。今回の例でいうと、プログラマンファミリーのメンバーが所属している会社・組織の件数を抽出しました。

programan_sisterとprograman_brotherはまだ学生なので、学生だけ2件数えられてそれ以外のメンバーに関しては仕事が別々なので1件ずつ返ってきています。

サンプルコード -->
1
2
3
User.joins(:job).group("jobs.name").count
SELECT COUNT(*) AS count_all, jobs.name AS jobs_name FROM `users` INNER JOIN `jobs` ON `jobs`.`id` = `users`.`job_id` GROUP BY jobs.name
=> {"プログラマン講師"=>1, "主婦"=>1, "保育士"=>1, "営業"=>1, "学生"=>2, "家電メーカー"=>1}

ここでもしかすると、Job.group(:name).countと同じ意味では??と思っている方がいるかも知れませんがそれは全然違います。Job.group(:name).countがどの様な返り値が返ってくるか見てみましょう。

job.nameのグループ化 -->
1
2
3
Job.group(:name).count
SELECT COUNT(*) AS count_all, `jobs`.`name` AS jobs_name FROM `jobs` GROUP BY `jobs`.`name`
=> {"プログラマン講師"=>1, "主婦"=>1, "保育士"=>1, "営業"=>1, "学生"=>1, "家電メーカー"=>1}

返り値を見ると学生も1件しかありません。なぜ1件かというと、Job.group(:name).countだとjobsテーブルのnameカラムのレコードの種類しか数えていない、つまり仕事の種類とその件数しか数えていません。

仕事(name)が被っているレコードが無い限り、それぞれデータは1件ずつ返ります。

しかしUser.joins(:job)とテーブル結合することによって、それぞれのメンバーが所属している仕事の種類でまとめられるので、programan_sisterとprograman_brotherが学生で重複している2件が出力されます。

このようにjoinsメソッドとgroupメソッドを併用して使えば、関連元テーブルと関連先テーブルのレコードを内部結合した状態で、データをまとめることができるので大変便利です。

この記事のまとめ

  • groupメソッドはカラムごとのデータをまとめて表示できるメソッド
  • いろいろな項目のランキングを作ることが出来る
  • groupは集計メソッドと併用して使える。併用した場合は、返り値はhash形式になる