集約関数とGROUP BY/HAVING
概要
- 日程: Day 1 / セッション 10
- 時間: 15:50-16:20
- 形式: 座学
- ゴール: COUNT/SUM/AVG/MAX/MIN それぞれの意味を例示でき、GROUP BY と HAVING の役割を WHERE と比較して説明できる
- 学習形式: 対話型解説
導入(5分)
ここまでで、皆さんは「テーブルから条件に合う行を取り出す」ことができるようになりました。「営業部の人」「給与40万以上の人」を抽出できます。
でも、上司が知りたいのはこういうことです:「営業部って何人いるの?」「全社員の平均給与は?」「いちばん高い商品の値段は?」「カテゴリごとの商品数を知りたい」。
これらは「行を1行ずつ返す」のではなく、「データの 集約結果 を返す」問いです。SQLには、まさにそのための関数群が用意されています。「集約関数」と「GROUP BY」「HAVING」——この3点セットを覚えると、皆さんはSQLの真の力に触れることになります。
本編(20分)
1. 集約関数の5つの基本選手
集約関数は「複数の行を、1つの値にまとめる」関数です。代表的な5つを覚えましょう。
| 関数 | 意味 | 例 |
|---|---|---|
COUNT(*) または COUNT(列) |
件数 | 何人いるか |
SUM(列) |
合計 | 給与の総額 |
AVG(列) |
平均 | 平均給与 |
MAX(列) |
最大値 | 最高給与 |
MIN(列) |
最小値 | 最低給与 |
-- 全社員の人数、合計給与、平均給与、最高給与、最低給与
SELECT COUNT(*) AS 人数,
SUM(salary) AS 給与合計,
AVG(salary) AS 平均給与,
MAX(salary) AS 最高給与,
MIN(salary) AS 最低給与
FROM employees;
AS 〇〇 は エイリアス(別名)で、結果列に好きな名前をつけられます。
ここがポイント
COUNT(*)は「行数」を数えるCOUNT(列名)は「その列が NULL でない行数」を数える(重要!)SUMAVGは数値列にしか使えないMAXMINは数値・文字列・日付に使える
コラム
集約関数の歴史で面白いのは、COUNT(*) と COUNT(列) の違いで何度も泣いた人がいる、という事実です。たとえば「メアドが登録されている顧客の数」を知りたくて COUNT(*) と書いてしまうと、メアド未入力の顧客もカウントされて数が合いません。正しくは COUNT(email)。NULL を含む列を COUNT(列) するとNULLは除外される——これは仕様であり、初心者が必ず一度ハマるポイントです。世界中の「あれ、件数が合わない…」の半分はこれが原因と言われています(諸説あります)。
2. GROUP BY:「グループに分けてから集約」
「全社員の平均給与」ではなく、「部署ごとの 平均給与」を出したいとき、GROUP BY の出番です。
SELECT department_id, AVG(salary) AS 平均給与
FROM employees
GROUP BY department_id;
これで「部署IDごとに行をまとめて、それぞれの平均給与」が出ます。
たとえると、トランプを「ジョーカーを抜いて、まずスートで4つの山に分けて、それぞれの山の枚数を数える」のと同じです。GROUP BY は「山に分ける」、集約関数は「それぞれの山で何かを計算する」。
SELECT に書ける列の制約
ここがGROUP BYの 最大のつまずきポイント。GROUP BY を使ったとき、SELECT 句に書ける列は次のどちらかに限られます:
GROUP BYで指定した列- 集約関数(COUNT/SUM/AVG/MAX/MIN)の中に入った列
-- OK
SELECT department_id, COUNT(*), AVG(salary)
FROM employees
GROUP BY department_id;
-- NG(first_nameはグループ化されていないし、集約もされていない)
SELECT department_id, first_name, COUNT(*)
FROM employees
GROUP BY department_id;
ここで少し考えてみてください。なぜ後者がダメなのでしょう?「営業部」という1つのグループには10人の社員がいて、first_name も10通りあります。「営業部の代表名前は誰?」という問いに答えがないので、データベースは「どれを返せばいいか分からない」と困ってエラーを返すのです。
3. HAVING:集約後の絞り込み
「平均給与が45万円以上の部署だけ知りたい」——このように 集約した結果 に対して条件をかけたいときが HAVING の出番です。
SELECT department_id, AVG(salary) AS 平均給与
FROM employees
GROUP BY department_id
HAVING AVG(salary) >= 450000;
ここで重要なのが、WHERE と HAVING の 使い分け です。
| 句 | いつ働くか | 対象 |
|---|---|---|
WHERE |
グループ化の 前、元の行を絞る | 個々の行 |
HAVING |
グループ化の 後、集約結果を絞る | グループ単位 |
実行順序のイメージはこうです:
(テーブル)"] --> B["WHERE
(行を絞る)"] B --> C["GROUP BY
(まとめる)"] C --> D["HAVING
(グループを絞る)"] D --> E["SELECT
(列を選ぶ)"] E --> F["ORDER BY
(並べる)"]
たとえば「営業部・開発部の社員に限定して、平均給与が45万以上の部署だけ」を出すなら:
SELECT department_id, AVG(salary) AS 平均給与
FROM employees
WHERE department_id IN (1, 2) -- 行レベルで絞る(営業部・開発部のみ)
GROUP BY department_id
HAVING AVG(salary) >= 450000; -- グループレベルで絞る(平均45万以上)
ここがポイント
WHEREは「行」を絞る、HAVINGは「グループ」を絞るHAVINGには集約関数を書ける(WHEREには書けない)- 順序は
WHERE → GROUP BY → HAVING
4. NULL と COUNT の落とし穴
集約関数を扱うときの NULL の挙動 を押さえておきましょう。
COUNT(*):NULL含めて全行を数えるCOUNT(列):その列がNULLでない行だけ数えるSUMAVGMAXMIN:NULL は 無視 される
-- 全社員数(NULL関係なくカウント)
SELECT COUNT(*) FROM employees;
-- email列がNULLでない社員数
SELECT COUNT(email) FROM employees;
salary がNULLの社員がいた場合、AVG(salary) はその社員を除いた平均を返します。「分母が違う」ことがあるので注意。
💬 AIに聞いてみよう
- 「
COUNT(*)とCOUNT(列)の使い分け、業務での実例で教えてください」 - 「
HAVINGを使わずにWHEREだけで同じことをやろうとすると、なぜダメなのか具体例で教えて」 - 「
GROUP BYの SELECT 制約を忘れて怒られました。なんでこの制約があるのか、設計者の気持ちを推測してください」
まとめ(5分)
一言でまとめると「集約関数で 数えて・足して・平均して 、GROUP BY で グループに分けて、HAVING で 集約結果を絞る」。WHERE は集約 前、HAVING は集約 後 の絞り込み——この順序のイメージが今日いちばん大事です。
次のセッションでは、今学んだ集約を実際に手を動かして書いていきます。「部署別の平均給与」「カテゴリ別の商品数」「月別の注文件数」——sandboxのデータに、いよいよ集計の目を向けます。そしてDay1全体の振り返りもします。
🔄 振り返りチェック
COUNT(*)とCOUNT(email)の違いを、1文で説明できますか?WHEREとHAVING、それぞれが効くタイミングを順序立てて言えますか?GROUP BY department_idを書いたとき、SELECT句にfirst_nameを書けない理由は?
補足資料
- 参考リンク: PostgreSQL: 集約関数
- 発展課題: customers テーブルで、メールアドレスが登録されている顧客の数と、全顧客数を別々にカウントし、差を求めるSQLを書く
学習ガイド
想定される質問と回答例
| 質問 | ヒント |
|---|---|
GROUP BY した結果を並べたいのですが? |
ORDER BY を後ろにつける。ORDER BY AVG(salary) DESC のように集約関数も使える |
HAVING COUNT(*) > 1 のような書き方はOK? |
OK。HAVINGには集約関数を使った条件を書ける。「2人以上いる部署だけ」のような用途で多用する |
部署名を出したいけど departments テーブルが必要 |
明日のJOINで扱う。今日は部署IDのみで集計する |
| 平均給与の小数点が長すぎる | ROUND(AVG(salary), 0) で四捨五入できる。AVG(salary)::numeric(10,0) のキャストも便利 |
つまずきやすいポイント
| つまずきポイント | ヒント |
|---|---|
GROUP BY でない列を SELECT に書いてエラー |
「グループ化していない列は集約関数の中に入れる」のルール |
WHERE COUNT(*) > 5 と書く |
これはエラー。集約関数は HAVING で使う |
HAVING だけ書いて GROUP BY を忘れる |
全行を1グループとして扱う動きにはなるが、意図と違うことがほとんど。セットで覚える |
COUNT 系の数値が予想と違う |
NULL の挙動が原因のことが多い。COUNT(*) と COUNT(列) の差を見る |
| 集約後の結果に行ごとの詳細も出したい | それはGROUP BYでは無理。次のJOINとサブクエリで扱う(Day2) |