GROUP BY と HAVING を正しく使う
GROUP BY の動作を理解する
GROUP BY? 指定した列の値が同じ行をまとめてグループ化する句。COUNT・SUM・AVGなどの集計関数と組み合わせて使う。「カテゴリ別の件数を集計」などに使用する。 は複数の行を「グループ」にまとめる処理です。同じ値を持つ行をひとつにまとめ、グループ単位で 集計関数? グループや全行に対して計算を行う関数。COUNT(件数)・SUM(合計)・AVG(平均)・MAX(最大)・MIN(最小)がある。GROUP BY と組み合わせて使うことが多い。 関数(COUNT、SUM、AVGなど)を計算します。
基本的な動作
-- ordersテーブル
-- order_id | customer_id | amount | status
-- 1 | 101 | 3000 | paid
-- 2 | 101 | 1500 | paid
-- 3 | 102 | 8000 | pending
-- 4 | 102 | 2000 | paid
SELECT customer_id, COUNT(*) AS order_count, SUM(amount) AS total
FROM orders
GROUP BY customer_id;
| customer_id | order_count | total |
|-------------|-------------|-------|
| 101 | 2 | 4500 |
| 102 | 2 | 10000 |
GROUP BY customer_id によって customer_id が同じ行がひとまとめになり、各グループに対してCOUNTとSUMが計算されます。
SELECT に書ける列の制限
GROUP BY を使う場合、SELECT句に書ける列は以下の2種類に限られます。
- GROUP BY に指定した列(グループキー)
- 集約関数の結果
-- OK: GROUP BYに指定したcustomer_idとSUM集計
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id;
-- エラー: statusはGROUP BYに含まれておらず、集約もされていない
SELECT customer_id, status, SUM(amount)
FROM orders
GROUP BY customer_id;
-- ERROR: 'status' isn't in GROUP BY
なぜこのルールがあるのでしょうか? GROUP BY customer_id でグループ化した後、customer_id=101 のグループには paid という status の行が2つあります。どちらの status を表示すべきかDBが判断できないため、エラーになります。
MySQLの ONLY_FULL_GROUP_BY モードがオフの場合、エラーにならず予測不能な値が返ることがあります。このモードは必ずオンにしておきましょう。
WHERE と HAVING の違い
HAVING句? GROUP BY後のグループに対して絞り込み条件を適用する句。`HAVING COUNT(*) >= 5` のように集計結果でフィルタする。行レベルの絞り込みはWHERE、グループへの絞り込みはHAVING。 と WHERE句? SELECT・UPDATE・DELETE文で行を絞り込む条件を指定する部分。`WHERE age >= 20 AND status = 'active'` のように複数条件を AND / OR で組み合わせられる。 の最大の違いは「適用されるタイミング」です。
| 句 | 適用タイミング | 使える条件 |
|---|---|---|
| WHERE | GROUP BY の前(行単位) | 集約関数は使えない |
| HAVING | GROUP BY の後(グループ単位) | 集約関数が使える |
-- WHEREとHAVINGの使い分け
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE status = 'paid' -- ① まず paid の行だけに絞る(行レベル)
GROUP BY customer_id
HAVING COUNT(*) >= 2; -- ② グループ化後、2件以上のグループだけ返す
よくある間違い
-- 間違い: 集約関数をWHEREに書く
SELECT customer_id, COUNT(*) AS cnt
FROM orders
WHERE COUNT(*) >= 2 -- エラー: WHEREでは集約関数は使えない
GROUP BY customer_id;
-- 正解: 集約関数の条件はHAVINGに書く
SELECT customer_id, COUNT(*) AS cnt
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 2; -- OK
ROLLUP・CUBE・GROUPING SETS
GROUP BY には小計・合計を自動生成する拡張機能があります。
ROLLUP
階層的な小計と総合計を自動生成します。
SELECT
COALESCE(category, '全カテゴリ') AS category,
COALESCE(CAST(YEAR(sale_date) AS CHAR), '全年') AS year,
SUM(amount) AS total
FROM sales
GROUP BY ROLLUP(category, YEAR(sale_date));
| category | year | total |
|-------------|--------|--------|
| 食品 | 2024 | 50000 |
| 食品 | 2025 | 60000 |
| 食品 | 全年 | 110000 | ← 食品の小計
| 家電 | 2024 | 80000 |
| 家電 | 2025 | 90000 |
| 家電 | 全年 | 170000 | ← 家電の小計
| 全カテゴリ | 全年 | 280000 | ← 総合計
CUBE
全ての組み合わせの小計を生成します。ROLLUPとは異なり、すべての次元の組み合わせを網羅します。
SELECT category, region, SUM(amount)
FROM sales
GROUP BY CUBE(category, region);
-- カテゴリ×地域, カテゴリのみ, 地域のみ, 総合計 の全パターンを返す
GROUPING SETS
小計を生成したい組み合わせを明示的に指定します。
SELECT category, region, SUM(amount)
FROM sales
GROUP BY GROUPING SETS (
(category, region), -- カテゴリ×地域
(category), -- カテゴリのみ
() -- 総合計
);
よくある間違いと修正方法
間違い1: HAVINGをWHEREの代わりに使う
-- 悪い例: 集約不要な条件をHAVINGに書く
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id
HAVING status = 'paid'; -- statusはグループキーでも集約関数でもない
-- 良い例: 集約前の条件はWHEREに書く
SELECT customer_id, SUM(amount)
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
WHEREで先に絞り込むことで、GROUP BYで処理する行数が減りパフォーマンスも向上します。
間違い2: COUNT(*) と COUNT(col) の混同
-- COUNT(*): NULL を含むすべての行をカウント
-- COUNT(col): NULL を除いた行をカウント
SELECT
COUNT(*) AS total_rows, -- 全行
COUNT(email) AS has_email, -- emailがNULLでない行
COUNT(DISTINCT email) AS unique_email -- ユニークなemail数
FROM customers;
間違い3: JOINで行が増えた後にCOUNTする
-- 問題: JOINで行が増えてCOUNTがズレる
SELECT c.customer_id, COUNT(o.order_id) AS order_count
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id -- ← 1注文に複数明細があるとorderが増える
GROUP BY c.customer_id;
-- 解決策1: DISTINCT を使う
SELECT c.customer_id, COUNT(DISTINCT o.order_id) AS order_count
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id
GROUP BY c.customer_id;
-- 解決策2: サブクエリで先に集計
SELECT c.customer_id, COALESCE(o.order_count, 0)
FROM customers c
LEFT JOIN (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
) o ON c.id = o.customer_id;
まとめ
- GROUP BY は行をグループにまとめ、集約関数の計算対象を作る
- SELECT には GROUP BY の列か集約関数の結果のみ書ける
- WHERE は行レベルのフィルタ(GROUP BY の前)、HAVING はグループレベルのフィルタ(GROUP BY の後)
- 行レベルの条件は WHERE に書く方がパフォーマンスも良い
- ROLLUP・CUBE・GROUPING SETS で小計・合計を自動生成できる
- COUNT(*) と COUNT(col) は NULL の扱いが異なる
次回は ウィンドウ関数? OVER()句を使って、グループ化せずに集計・順位付けを行う関数。ROW_NUMBER()・RANK()・LAG()・SUM() OVER() など。GROUP BYと違い、元の行を保持したまま計算できる。 について、GROUP BY との違いやOVER句の使い方を解説します。