GROUP BYとHAVINGの違いとWHERE句との使い分け

black laptop computer turned-on displaying source code on table SQL

GROUP BY は SQL で集計クエリを書くときの中心的な構文です。「HAVING と WHERE をどう使い分けるのか」「集約関数の条件はどちらに書くのか」は、SQL を学び始めると必ず直面するポイントです。この記事では GROUP BY の基本構文から始め、WHERE と HAVING の実行タイミングの根本的な違い、正しい使い分け方、よくある誤用パターンを実例と合わせて解説します。MySQL 8.0 以上を想定しています。

このシナリオで考える

以下の orders(受注)テーブルを使います。顧客 ID・注文金額・注文日の 3 列を持つシンプルな構成です。

CREATE TABLE orders (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  customer_id INT           NOT NULL,
  amount      DECIMAL(10,2) NOT NULL,
  ordered_at  DATE          NOT NULL
);

INSERT INTO orders (customer_id, amount, ordered_at) VALUES
  (1, 1200, '2025-01-05'),
  (1, 3500, '2025-01-20'),
  (2,  800, '2025-01-10'),
  (3, 2000, '2025-02-01'),
  (3, 5000, '2025-02-15'),
  (3, 1500, '2025-03-01'),
  (4,  400, '2025-03-10'),
  (2,  600, '2025-03-15');

顧客 1 は 2 回、顧客 2 は 2 回、顧客 3 は 3 回、顧客 4 は 1 回注文しているデータです。このデータを使って GROUP BY と各種フィルタ条件の動きを確認していきます。

GROUP BYの基本:集計を行うしくみ

GROUP BY は指定した列の値が同じ行をひとつのグループにまとめます。グループごとに COUNT・SUM・AVG・MAX・MIN といった集約関数(aggregate function)を適用できます。

SELECT
  customer_id,
  COUNT(*)    AS order_count,
  SUM(amount) AS total_amount,
  AVG(amount) AS avg_amount
FROM orders
GROUP BY customer_id;
+-------------+-------------+--------------+------------+
| customer_id | order_count | total_amount | avg_amount |
+-------------+-------------+--------------+------------+
|           1 |           2 |      4700.00 |  2350.0000 |
|           2 |           2 |      1400.00 |   700.0000 |
|           3 |           3 |      8500.00 |  2833.3333 |
|           4 |           1 |       400.00 |   400.0000 |
+-------------+-------------+--------------+------------+

SELECT に書けるのは「GROUP BY で指定した列」か「集約関数」の 2 種類だけです。MySQL 8.0 のデフォルトでは ONLY_FULL_GROUP_BY モードが有効なため、それ以外の列を SELECT に含めるとエラーになります。

-- NG: ordered_at は GROUP BY 列でも集約関数でもない
SELECT customer_id, ordered_at, COUNT(*)
FROM orders
GROUP BY customer_id;
-- ERROR 1055: 'orders.ordered_at' isn't in GROUP BY

「顧客ごとに何件注文があるか」を取りたいのに ordered_at をそのまま SELECT に書いてしまうのは、GROUP BY の理解が不十分なときに起きやすいミスです。集約した結果、1 行に圧縮されたグループの中に ordered_at の値が複数あるため、どの値を返すかが決まらないのが理由です。

WHEREとHAVINGの違い:実行タイミングで理解する

WHERE と HAVING はどちらも「絞り込み」に使いますが、実行されるタイミングが根本的に異なります。

実行タイミング フィルタ対象 集約関数
WHERE GROUP BY 行(Row) 使えない
HAVING GROUP BY グループ(Group) 使える

SQL の論理的な実行順序は次のとおりです。

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

WHERE は集約が始まる前に動くため、SUM(amount)COUNT(*) といった集約関数を条件に書けません。HAVING は集約後に動くため、集約関数を条件として使えます。

-- NG: WHERE に集約関数は書けない
SELECT customer_id
FROM orders
WHERE COUNT(*) > 2
GROUP BY customer_id;
-- ERROR 1111: Invalid use of group function

-- OK: 集約後の条件は HAVING に書く
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 2;
+-------------+-------------+
| customer_id | order_count |
+-------------+-------------+
|           3 |           3 |
+-------------+-------------+

3 回以上注文した顧客は顧客 3 だけです。HAVING を使うことで、集約後のカウント値に対して条件を適用できました。

WHEREとHAVINGを組み合わせる

「2025年1月の注文だけを対象として、2 回以上注文した顧客を取り出す」という要件を考えます。

SELECT
  customer_id,
  COUNT(*)    AS order_count,
  SUM(amount) AS total_amount
FROM orders
WHERE ordered_at BETWEEN '2025-01-01' AND '2025-01-31'  -- ① 行を先に絞る
GROUP BY customer_id
HAVING COUNT(*) >= 2;                                    -- ② グループを絞る
+-------------+-------------+--------------+
| customer_id | order_count | total_amount |
+-------------+-------------+--------------+
|           1 |           2 |      4700.00 |
+-------------+-------------+--------------+

① WHERE で 1 月分の行だけを残してからグループ化します。② HAVING で集約後に「2 件以上」のグループだけを選びます。WHERE が先に動くため、2 月・3 月の注文は集計対象から外れています。WHERE と HAVING は役割が違うので、1 つのクエリに両方書くことも自然な書き方です。

HAVINGの実用パターン

HAVING でよく使う集約条件のパターンを表で整理します。

やりたいこと HAVING の条件例
合計が一定以上のグループを取る HAVING SUM(amount) >= 5000
件数が多いグループを取る HAVING COUNT(*) > 2
平均が一定以上のグループを取る HAVING AVG(amount) >= 1000
最小値が閾値を超えるグループだけ HAVING MIN(amount) > 500
重複データのあるグループを検出 HAVING COUNT(*) > 1

最後の「重複データ検出」は実務で特に役立ちます。メールアドレスや電話番号など、一意であるべき列に重複が混入していないかを調べるときに使えます。

-- メールアドレスが重複しているレコードを検出する
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

このクエリで email の値が同じ行が 2 行以上あるものだけを抽出できます。データクレンジングの際に重宝するパターンです。

よくある落とし穴と注意点

HAVINGに集約関数を使わない条件を書く

MySQL では HAVING に集約関数を含まない条件を書いても動きますが、推奨できません。全行をグループ化してから絞り込むため、先に WHERE で絞った場合より処理が増えます。

-- 動くが非推奨
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id
HAVING customer_id > 2;

-- 推奨:先に WHERE で絞るとグループ化するデータが減る
SELECT customer_id, SUM(amount)
FROM orders
WHERE customer_id > 2
GROUP BY customer_id;

GROUP BY列のNULL扱い

GROUP BY の対象列に NULL が含まれると、NULL どうしはひとつのグループにまとめられます。通常の比較演算では NULL = NULL は真にならない三値論理(three-valued logic)の世界ですが、GROUP BY では例外的に「同じ値」として扱われます。集計結果に NULL が現れたときは、対象列にデータが欠損していないか確認しましょう。

まとめ

  • GROUP BY は行をグループ化し、集約関数を使って集計するための構文です
  • WHERE は GROUP BY の前に動く行フィルタ、HAVING は GROUP BY の後に動くグループフィルタです
  • 集約関数を使う条件は HAVING に、使わない条件は WHERE に書くのが基本です
  • HAVING に集約しない条件を書いても動きますが、WHERE に移した方が効率的です
  • GROUP BY 列の NULL はひとつのグループにまとめられます

参考リンク

アイキャッチ画像: Photo by Jantine Doornbos on Unsplash

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