#05 SQLをちゃんと理解する

GROUP BY と HAVING を正しく使う

GROUP BY の動作を理解する

GROUP BY? 指定した列の値が同じ行をまとめてグループ化する句。COUNT・SUM・AVGなどの集計関数と組み合わせて使う。「カテゴリ別の件数を集計」などに使用する。 は複数の行を「グループ」にまとめる処理です。同じ値を持つ行をひとつにまとめ、グループ単位で 集計関数? グループや全行に対して計算を行う関数。COUNT(件数)・SUM(合計)・AVG(平均)・MAX(最大)・MIN(最小)がある。GROUP BY と組み合わせて使うことが多い。 関数(COUNT、SUM、AVGなど)を計算します。

GROUP BY 前後のデータ変換 GROUP BY 前(ordersテーブル) order_id customer_id amount 1 101 3,000 2 101 1,500 3 102 8,000 4 102 2,000 5 103 5,500 5行のデータ GROUP BY customer_id GROUP BY 後 customer_id COUNT(*) SUM(amount) 101 2 4,500 102 2 10,000 103 1 5,500 3行(顧客ID数分)に集約 GROUP BY のルール SELECT に書ける列 ① GROUP BY に指定した列 ② 集約関数の結果 (COUNT/SUM/AVG/MAX/MIN) SELECT に書けない列 GROUP BY にない非集約列 (どの値を返すか決定できない) 例: status, name など HAVING の条件 集約関数を使った条件 例: COUNT(*) >= 2   SUM(amount) > 10000
図1: GROUP BY前後のデータ変換イメージ

基本的な動作

-- 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種類に限られます。

  1. GROUP BY に指定した列(グループキー)
  2. 集約関数の結果
-- 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 の違い

WHERE vs HAVING の適用タイミング 全データ id=1 paid 3000 id=2 pend 1500 id=3 paid 8000 id=4 paid 2000 id=5 pend 5500 WHERE WHERE適用後 status='paid'で絞込 id=1 paid 3000 id=3 paid 8000 id=4 paid 2000 3行に削減 GROUP GROUP BY後 customer_idでグループ化 cust=101 cnt=2 cust=102 cnt=1 2グループ HAVING HAVING適用後 COUNT(*) >= 2 で絞込 cust=101 cnt=2 ✓ cust=102 cnt=1 ✗ 1グループのみ 結果 cust=101 cnt=2 WHERE と HAVING の比較 項目 WHERE HAVING 適用タイミング GROUP BY の前(行単位) GROUP BY の後(グループ単位) 集約関数の使用 使えない(COUNT(*) はエラー) 使える(COUNT(*) >= 2 など) パフォーマンス 先に絞り込むため速い 集約後なので行数は既に少ない 使うべき場面 status='paid'など行の条件 COUNT(*)>=2など集計後の条件 行レベルの条件は必ずWHEREに書く → GROUP BYで処理する行数を減らしパフォーマンスを向上
図2: WHEREはGROUP BY前、HAVINGはGROUP BY後に適用される

HAVING句? GROUP BY後のグループに対して絞り込み条件を適用する句。`HAVING COUNT(*) >= 5` のように集計結果でフィルタする。行レベルの絞り込みはWHERE、グループへの絞り込みはHAVING。 WHERE句? SELECT・UPDATE・DELETE文で行を絞り込む条件を指定する部分。`WHERE age >= 20 AND status = 'active'` のように複数条件を AND / OR で組み合わせられる。 の最大の違いは「適用されるタイミング」です。

適用タイミング使える条件
WHEREGROUP BY の前(行単位)集約関数は使えない
HAVINGGROUP 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句の使い方を解説します。