📖 テーマ設定
🔊 音声設定
1.2
1.0
1.0
▶️ 再生コントロール
🎵 BGM設定
0.3
🔔 効果音設定
0.3

集約関数と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 でない行数」を数える(重要!)
  • SUM AVG は数値列にしか使えない
  • MAX MIN は数値・文字列・日付に使える

コラム

集約関数の歴史で面白いのは、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 句に書ける列は次のどちらかに限られます:

  1. GROUP BY で指定した列
  2. 集約関数(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;

ここで重要なのが、WHEREHAVING使い分け です。

いつ働くか 対象
WHERE グループ化の 、元の行を絞る 個々の行
HAVING グループ化の 、集約結果を絞る グループ単位

実行順序のイメージはこうです:

flowchart LR A["FROM
(テーブル)"] --> 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でない行だけ数える
  • SUM AVG MAX MIN :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文で説明できますか?
  • WHEREHAVING、それぞれが効くタイミングを順序立てて言えますか?
  • 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)
読み上げを開始します...

AIに質問する