Python | DB・SQL:GROUP BY

Python
スポンサーリンク

概要(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;
SQL

user_name は GROUP BY に含まれている
SUM(minutes) は集計関数

一方、次のようなクエリは NG です(エラーになる DB が多い)。

SELECT user_name, subject, SUM(minutes)
FROM study_logs
GROUP BY user_name;
SQL

subject は GROUP BY に含まれていないし、集計関数でもない
→ 「どの subject を表示すればいいか分からない」状態になる

なぜかというと、user_name ごとにグループを作ると、
Taro のグループには math と english の 2 行が入っています。
そのとき、「subject を 1 つだけ表示しろ」と言われても、
どれを選べばいいか決められないからです。

このルールは最初つまずきやすいですが、
「グループごとに 1 行しか返さないのだから、グループ内でバラバラな値はそのまま出せない」
とイメージすると納得しやすくなります。


実務的な GROUP BY の使いどころ

「集計画面」「ダッシュボード」はほぼ全部 GROUP BY

現実のアプリで GROUP BY が使われるのは、だいたいこんな場面です。

ユーザーごとの売上合計
日付ごとのアクセス数
カテゴリごとの商品数
タグごとの記事数
ユーザーごとの合計勉強時間

こういう「集計」「ランキング」「統計」系の画面は、
裏側でほぼ必ず GROUP BY + 集計関数が動いています。

Django なら annotatevalues を使って書くことが多いですが、
頭の中では「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 に含まれる列」か「集計関数で包まれた列」だけ、というルールを強く意識する。

タイトルとURLをコピーしました