概要(GROUP BY は「グループごとにまとめて計算する」ためのスイッチ)
GROUP BY は、SQL で「グループごとに集計する」ための仕組みです。
「ユーザーごとの合計金額」「日付ごとの件数」「科目ごとの勉強時間」みたいな集計をするときに必ず出てきます。
SELECT は本来「行をそのまま返す」コマンドですが、
GROUP BY と集計関数(COUNT, SUM, AVG, MAX, MIN など)を組み合わせると、
「行をまとめて、グループごとの結果だけを返す」
というモードに切り替わります。
ここをちゃんと理解できると、Django での集計処理(annotate, aggregate)の裏側も一気に見通しがよくなります。
集計のイメージを先に固める
例となるテーブルを決める
まずは、イメージしやすいテーブルを一つ決めましょう。
学習ログのテーブル study_logs を例にします。
study_logs テーブル
id | user_name | subject | minutes | date
---+-----------+---------+---------+------------
1 | Taro | math | 60 | 2025-01-19
2 | Taro | english | 30 | 2025-01-19
3 | Hanako | math | 45 | 2025-01-19
4 | Taro | math | 40 | 2025-01-20
5 | Hanako | english | 50 | 2025-01-20
このテーブルから、例えば次のようなことを知りたいとします。
「ユーザーごとの合計勉強時間」
「科目ごとの合計勉強時間」
「ユーザー×科目ごとの合計勉強時間」
こういう「グループごとの集計」をやるときに GROUP BY が登場します。
GROUP BY の基本形と COUNT の組み合わせ
「グループごとに 1 行」に変わる
GROUP BY の基本形はこうです。
SELECT グループのキー, 集計関数(...)
FROM テーブル名
GROUP BY グループのキー;
SQL例えば、「ユーザーごとのログ件数」を知りたいとします。
このときは COUNT と GROUP BY を組み合わせます。
SELECT user_name, COUNT(*) AS log_count
FROM study_logs
GROUP BY user_name;
SQL結果はこうなります。
user_name | log_count
----------+----------
Taro | 3
Hanako | 2
ここで起きていることを、しっかりイメージしてほしいです。
user_name ごとに行をグループ分けする
各グループの中で COUNT(*) を計算する
グループごとに 1 行だけ結果を返す
つまり、「Taro の行を全部まとめて 1 行」「Hanako の行を全部まとめて 1 行」にしているわけです。
SUM / AVG など他の集計関数との組み合わせ
合計(SUM)で「トータル時間」を出す
今度は、「ユーザーごとの合計勉強時間」を出してみます。
minutes の合計を取りたいので、SUM を使います。
SELECT user_name, SUM(minutes) AS total_minutes
FROM study_logs
GROUP BY user_name;
SQL結果はこうなります。
user_name | total_minutes
----------+--------------
Taro | 130 -- 60 + 30 + 40
Hanako | 95 -- 45 + 50
GROUP BY user_name によって、
「Taro グループ」「Hanako グループ」に分かれ、
それぞれのグループ内で SUM(minutes) が計算されています。
平均(AVG)や最大・最小(MAX / MIN)
同じように、平均時間を出したいなら AVG を使います。
SELECT user_name, AVG(minutes) AS avg_minutes
FROM study_logs
GROUP BY user_name;
SQL最大・最小を出したいなら MAX / MIN です。
SELECT user_name,
MAX(minutes) AS max_minutes,
MIN(minutes) AS min_minutes
FROM study_logs
GROUP BY user_name;
SQLポイントは、「GROUP BY でグループを作り、そのグループごとに集計関数を適用する」という流れです。
集計関数は「グループの中の複数行 → 1 つの値」に変換する役割を持っています。
複数列で GROUP BY して「組み合わせごと」に集計する
ユーザー×科目ごとの合計時間
今度は、「ユーザー×科目ごとの合計勉強時間」を出してみます。
つまり、「Taro の math は合計何分?」「Taro の english は?」「Hanako の math は?」…という感じです。
このときは、GROUP BY に複数列を指定します。
SELECT user_name, subject, SUM(minutes) AS total_minutes
FROM study_logs
GROUP BY user_name, subject;
SQL結果はこうなります。
user_name | subject | total_minutes
----------+---------+--------------
Taro | math | 100 -- 60 + 40
Taro | english | 30
Hanako | math | 45
Hanako | english | 50
ここでのイメージはこうです。
user_name と subject の組み合わせごとにグループを作る
(Taro×math、Taro×english、Hanako×math、Hanako×english)
各グループ内で SUM(minutes) を計算する
GROUP BY に並べた列の組み合わせが「グループのキー」になります。
Django の values("user_name", "subject").annotate(total_minutes=Sum("minutes")) が、まさにこれと同じことをしています。
WHERE と GROUP BY と HAVING の違い
WHERE は「グループを作る前の行」を絞る
例えば、「2025-01-19 のログだけを対象に、ユーザーごとの合計時間を出したい」とします。
このときは、まず WHERE で対象の日付に絞ります。
SELECT user_name, SUM(minutes) AS total_minutes
FROM study_logs
WHERE date = '2025-01-19'
GROUP BY user_name;
SQL処理の流れはこうです。
FROM でテーブルを読む
WHERE で「2025-01-19 の行だけ」に絞る
GROUP BY で user_name ごとにグループを作る
各グループで SUM(minutes) を計算する
WHERE は「グループを作る前に、どの行を残すか」を決めるフィルターです。
HAVING は「グループを作った後の結果」を絞る
今度は、「ユーザーごとの合計時間を出しつつ、合計が 100 分以上のユーザーだけに絞りたい」とします。
このときに使うのが HAVING です。
SELECT user_name, SUM(minutes) AS total_minutes
FROM study_logs
GROUP BY user_name
HAVING SUM(minutes) >= 100;
SQL結果はこうなります。
user_name | total_minutes
----------+--------------
Taro | 130
Hanako は 95 分なので、HAVING の条件に合わず除外されます。
ここでのポイントは、「HAVING では集計結果に対して条件を書ける」ということです。
WHERE では SUM(minutes) のような集計関数は使えません(まだグループができていないから)。
ざっくり言うと、
WHERE → グループを作る前に行を絞る
HAVING → グループを作った後にグループを絞る
という役割分担になっています。
GROUP BY を使うときに絶対に意識してほしいルール
SELECT に書ける列は「グループのキー」か「集計関数だけ」
多くのデータベースでは、GROUP BY を使うときに次のルールがあります。
「SELECT に書ける列は、GROUP BY に含まれている列か、集計関数で包まれた列だけ」
例えば、次のクエリは OK です。
SELECT user_name, SUM(minutes)
FROM study_logs
GROUP BY user_name;
SQLuser_name は GROUP BY に含まれている
SUM(minutes) は集計関数
一方、次のようなクエリは NG です(エラーになる DB が多い)。
SELECT user_name, subject, SUM(minutes)
FROM study_logs
GROUP BY user_name;
SQLsubject は GROUP BY に含まれていないし、集計関数でもない
→ 「どの subject を表示すればいいか分からない」状態になる
なぜかというと、user_name ごとにグループを作ると、
Taro のグループには math と english の 2 行が入っています。
そのとき、「subject を 1 つだけ表示しろ」と言われても、
どれを選べばいいか決められないからです。
このルールは最初つまずきやすいですが、
「グループごとに 1 行しか返さないのだから、グループ内でバラバラな値はそのまま出せない」
とイメージすると納得しやすくなります。
実務的な GROUP BY の使いどころ
「集計画面」「ダッシュボード」はほぼ全部 GROUP BY
現実のアプリで GROUP BY が使われるのは、だいたいこんな場面です。
ユーザーごとの売上合計
日付ごとのアクセス数
カテゴリごとの商品数
タグごとの記事数
ユーザーごとの合計勉強時間
こういう「集計」「ランキング」「統計」系の画面は、
裏側でほぼ必ず GROUP BY + 集計関数が動いています。
Django なら annotate や values を使って書くことが多いですが、
頭の中では「GROUP BY して SUM してるな」とイメージできると、
クエリの意味が一気にクリアになります。
いきなり複雑にしないで、「段階的に」考える
GROUP BY を使うときも、いきなり 1 行で複雑に書かない方が理解しやすいです。
例えば、「ユーザー×科目ごとの合計時間で、合計 60 分以上のものだけを、合計時間の多い順に並べたい」とします。
日本語で分解すると、
ユーザー×科目ごとに minutes を合計する
合計が 60 分以上のグループだけに絞る
合計時間の多い順に並べる
これを SQL に落とすと、こうなります。
SELECT user_name, subject, SUM(minutes) AS total_minutes
FROM study_logs
GROUP BY user_name, subject
HAVING SUM(minutes) >= 60
ORDER BY total_minutes DESC;
SQLいきなりこれを書こうとするのではなく、
まずは GROUP BY だけで合計を出す
次に HAVING を足して 60 分以上に絞る
最後に ORDER BY を足して並び替える
というふうに、段階的に育てていくと、
「どこで何をしているか」が見えやすくなります。
まとめ(GROUP BY は「行をまとめて、グループごとに 1 行にする」)
GROUP BY を初心者目線でまとめると、こうなります。
GROUP BY は「指定した列の値ごとに行をグループ分けし、グループごとに 1 行だけ結果を返す」ための仕組み。
COUNT, SUM, AVG, MAX, MIN などの集計関数と組み合わせることで、「ユーザーごとの合計」「日付ごとの件数」「カテゴリごとの平均」などを簡単に出せる。
複数列を GROUP BY すると、「ユーザー×科目」などの組み合わせごとに集計できる。
WHERE はグループを作る前の行を絞り、HAVING はグループを作った後の集計結果を絞る。
SELECT に書けるのは「GROUP BY に含まれる列」か「集計関数で包まれた列」だけ、というルールを強く意識する。
