HAVING — GROUP BY で集計した後、注文2件以上のユーザーだけを絞り込む
基礎編で学んだ GROUP BY + 集計関数の次のステップが HAVING です。HAVING は GROUP BY 後の集計結果に条件をかけるための句で、集計関数(COUNT・SUM など)を条件式に使えます。
SELECT 列名, COUNT(...), SUM(...) FROM users AS u INNER JOIN orders AS o ON u.user_id = o.user_id GROUP BY u.user_id, u.name HAVING COUNT(o.order_id) >= 2;
WHERE は JOIN後・GROUP BY前に個々の行を評価します。集計関数は使えません。HAVING は GROUP BY後に各グループの集計結果を評価します。集計関数が使えます。SQL の実行順序: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
users と orders を結合し、注文件数が2件以上のユーザーの名前・注文件数・合計金額を取得してください。合計金額の多い順で出力します。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 1 | 1500 |
| 3 | 2 | 5000 |
| 4 | 3 | 2000 |
| 5 | 3 | 800 |
| name | order_count | total_amount |
|---|---|---|
| 田中 | 2 | 4500 |
| 山田 | 2 | 2800 |
SELECT u.name, COUNT(o.order_id) AS order_count, -- NULL以外の行を数える SUM(o.amount) AS total_amount -- 注文金額を合計する FROM users AS u INNER JOIN orders AS o ON u.user_id = o.user_id GROUP BY u.user_id, u.name -- SELECT の非集計列を全列列挙する HAVING COUNT(o.order_id) >= 2 -- GROUP BY後の集計値で絞り込む ORDER BY total_amount DESC; /* 実行順序: 1. FROM users AS u → users を読み込む 2. INNER JOIN orders AS o → ON で結合 3. GROUP BY u.user_id → ユーザーでグループ化 4. COUNT / SUM → 各グループで集計 5. HAVING COUNT(...) → 件数2以上を残す 6. SELECT u.name, ... → 列を射影 7. ORDER BY total_amount DESC → 合計の降順でソート */
LEGEND
① FROM
FROM users AS uusers テーブル(3行)を左テーブルとして読み込みます。この後 orders と JOIN し、集計・HAVING フィルタへと進みます。| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
u = users(左) o = orders(右)
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。WHERE は GROUP BY の前に個々の行を評価するため集計関数が使えません。HAVING は GROUP BY の後に各グループの集計値を評価するため COUNT・SUM などをそのまま条件に書けます。WHERE COUNT(...) >= 2 と書くとエラーになります。HAVING COUNT(...) >= 2 AND SUM(...) >= 5000 と書けます。さらに WHERE で集計前フィルタ、HAVING で集計後フィルタ、という2段階の絞り込みも実務で頻出のパターンです。WHERE COUNT(o.order_id) >= 2 と書くとエラーになります。集計値による絞り込みには必ず HAVING を使います。逆に特定ユーザーへの絞り込みは HAVING でも動きますが、GROUP BY 前に絞り込む WHERE に書く方がパフォーマンス上有利です。SELECT COUNT(o.order_id) AS cnt ... HAVING cnt >= 2 は PostgreSQL ではエラーになります(MySQL は許容)。HAVING は SELECT より先に評価されるためエイリアスがまだ存在しません。LEFT JOIN + COALESCE — 注文0件のユーザーも含め、全ユーザーの集計結果を返す
「注文ゼロのユーザーも 0 として一覧に含めたい」という要件は、LEFT JOIN + COALESCE の組み合わせで解決します。基礎編で学んだ LEFT JOIN の NULL 補完を、集計関数と COALESCE でさらに実用的に活用します。
SELECT u.name, COUNT(o.order_id) AS order_count, -- NULLをスキップして0を返す COALESCE(SUM(o.amount), 0) AS total_amount -- SUM(NULL)→NULL を 0 に変換 FROM users AS u LEFT JOIN orders AS o ON u.user_id = o.user_id GROUP BY u.user_id, u.name;
COUNT(o.order_id) は NULL を無視 して数えるため、注文0件ユーザーは自動で 0 になります。COUNT(*) は行数を数えるため、LEFT JOIN で補完された NULL 行も 1 としてカウントしてしまいます。LEFT JOIN 後の集計では 必ず
COUNT(右テーブルの列名) を使います。users テーブルの全ユーザーについて、注文件数(order_count)と合計金額(total_amount)を取得してください。注文が0件のユーザーは 0 として表示します。合計金額の降順で出力します。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 1 | 1500 |
| 3 | 2 | 5000 |
| name | order_count | total_amount |
|---|---|---|
| 佐藤 | 1 | 5000 |
| 田中 | 2 | 4500 |
| 山田 | 0 | 0 |
SELECT u.name, COUNT(o.order_id) AS order_count, -- COUNT(列名)はNULLをスキップ → 0件ユーザーは自動で0になる COALESCE(SUM(o.amount), 0) AS total_amount -- SUM(NULL) = NULL のため COALESCE で 0 に変換する FROM users AS u LEFT JOIN orders AS o -- 全ユーザーを保持(山田は NULL 補完) ON u.user_id = o.user_id GROUP BY u.user_id, u.name -- NULL 行も含めてグループ化する ORDER BY total_amount DESC; /* 実行順序: 1. FROM users AS u → users を読み込む 2. LEFT JOIN orders AS o → ON で照合・NULL 補完 3. GROUP BY u.user_id, u.name → ユーザーでグループ化 4. COUNT / COALESCE(SUM,0) → 件数・合計(NULLは0補正) 5. SELECT / ORDER BY → total_amount DESC で出力 */
LEGEND
① FROM
FROM users AS uusers テーブル(3行)を左テーブルとして読み込みます。LEFT JOIN のため全行が必ず結果に含まれます。| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
u = users(左) o = orders(右)
COUNT(o.order_id) は NULL を数えない ため、LEFT JOIN で NULL 補完された山田のグループは自動で 0 になります。一方 COUNT(*) は行の存在をカウントするため NULL 行も 1 として数えてしまいます。LEFT JOIN 後の集計では右テーブルの NOT NULL な列(主キーなど)を COUNT の引数に使うのが正しいパターンです。SUM は集計対象が全て NULL の場合に NULL を返します。COALESCE(値, デフォルト) は最初の非 NULL 値を返す関数なので COALESCE(NULL, 0) = 0 となります。なお SUM(COALESCE(o.amount, 0)) と順序を逆にすると「NULL の amount を 0 として合計に含める」別の意味になるため、COALESCE で SUM 全体を包む順序が重要です。COUNT(*) を使うと山田の order_count が 1 になり誤った集計になります。LEFT JOIN を使う集計では必ず COUNT(右テーブルの列名) を使う習慣をつけましょう。LEFT JOIN ... WHERE o.amount > 0 のように書くと、NULL 行(山田)は NULL > 0 が FALSE になり除外されます。LEFT JOIN の効果を保ったまま右テーブルに条件をかけたい場合は ON 句に書きます。SELF JOIN — 同じテーブルを2つのエイリアスで結合し、上司と部下の名前を取得する
SELF JOIN(自己結合)は、同じテーブルを2回 JOIN する操作です。テーブルに別々のエイリアスを付けることで、同一テーブル内の行同士の関係(上司と部下、親カテゴリと子カテゴリ)を表現できます。
FROM employees AS e -- e = 部下ロール INNER JOIN employees AS m -- m = 上司ロール(同じテーブル) ON e.manager_id = m.emp_id; -- 部下のmanager_idが上司のemp_idと一致
manager_id のような自分自身のテーブルを参照する外部キー(自己参照)を持たせることで、階層構造を1テーブルで表現できます。SELF JOIN はこの構造から親・子の関係を取得するための定番テクニックです。employees テーブルから、各社員の名前(employee)と直属の上司の名前(manager)を取得してください。上司がいない社員(社長:田中)は除外します。
| emp_id | name | manager_id |
|---|---|---|
| 1 | 田中 | NULL |
| 2 | 佐藤 | 1 |
| 3 | 山田 | 1 |
| 4 | 鈴木 | 2 |
| 5 | 高橋 | 2 |
| employee | manager |
|---|---|
| 佐藤 | 田中 |
| 山田 | 田中 |
| 鈴木 | 佐藤 |
| 高橋 | 佐藤 |
SELECT e.name AS employee, -- 部下側エイリアス e の name m.name AS manager -- 上司側エイリアス m の name FROM employees AS e INNER JOIN employees AS m -- 同じテーブルを別ロールで結合: e.manager_id = m.emp_id で上司を特定 ON e.manager_id = m.emp_id -- manager_id=NULL(田中)は ON条件不成立 → 除外 ORDER BY e.manager_id, e.emp_id; /* 実行順序: 1. FROM employees AS e → 全5行を「部下ロール」として読み込む 2. INNER JOIN employees AS m → 各行の manager_id を emp_id として上司を照合 manager_id=NULL(田中)はON条件を満たさないため除外 3. SELECT e.name, m.name → 部下名・上司名の2列を射影 4. ORDER BY → manager_id → emp_id の順でソート */
LEGEND
① FROM
FROM employees AS e(部下ロール)employees テーブル(5行)を「部下ロール(e)」として読み込みます。manager_id 列が SELF JOIN のキーです。manager_id=NULL(田中)は上司を持たない社長です。| emp_id | name | manager_id |
|---|---|---|
| 1 | 田中 | NULL |
| 2 | 佐藤 | 1 |
| 3 | 山田 | 1 |
| 4 | 鈴木 | 2 |
| 5 | 高橋 | 2 |
e = 部下ロール m = 上司ロール(どちらも employees)
e(部下)の manager_id と m(上司)の emp_id を ON 句でつなぐことで、「部下の上司は誰か」という行間の関係が取得できます。INNER JOIN のため manager_id=NULL(社長)は自動的に除外されます。employees テーブルのように「自分自身のテーブルを参照する外部キー」を持つ構造を隣接リストモデルといいます。SELF JOIN で取得できるのは直属1階層のみです。孫・曾孫など任意の深さを辿るには WITH RECURSIVE(再帰 CTE)が必要で、PostgreSQL・MySQL 8.0以降で利用できます。LEFT JOIN employees AS m ON e.manager_id = m.emp_id に変えるだけで実現できます。WITH RECURSIVE に発展しますが、直属1階層のうちは SELF JOIN が最もシンプルな解法です。JOIN + CASE式 — 注文ステータスを日本語ラベルに変換しながらユーザー名と結合する
CASE式は SQL の条件分岐式で、列の値に応じて異なる値を返せます。JOIN と組み合わせることで、結合と同時に値の変換・ラベル付けができます。
SELECT 列名, CASE 列名 -- 単純CASE式: 列の値で分岐 WHEN 'pending' THEN '処理中' WHEN 'shipped' THEN '発送済み' ELSE '不明' -- 未定義ステータスへのフォールバック END AS status_label
CASE 列名 WHEN 値 THEN ...(単純): 列の値と定数を = で比較します。本問のように列挙した値に対応する変換に向いています。CASE WHEN 条件式 THEN ...(検索): 任意の条件式(>・LIKE・IS NULL 等)が使えます。範囲分類や複合条件に向いています。orders テーブルと users テーブルを結合し、注文一覧を取得してください。status 列の英語値を CASE式で日本語ラベル(status_label)に変換して出力します。
| order_id | user_id | amount | status |
|---|---|---|---|
| 1 | 1 | 3000 | shipped |
| 2 | 1 | 1500 | pending |
| 3 | 2 | 5000 | delivered |
| 4 | 3 | 2000 | cancelled |
| 5 | 3 | 800 | pending |
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_name | amount | status_label |
|---|---|---|---|
| 1 | 田中 | 3000 | 発送済み |
| 2 | 田中 | 1500 | 処理中 |
| 3 | 佐藤 | 5000 | 配達完了 |
| 4 | 山田 | 2000 | キャンセル |
| 5 | 山田 | 800 | 処理中 |
SELECT o.order_id, u.name AS user_name, o.amount, CASE o.status -- 単純CASE式: o.status の値で分岐して日本語ラベルに変換する WHEN 'pending' THEN '処理中' WHEN 'shipped' THEN '発送済み' WHEN 'delivered' THEN '配達完了' WHEN 'cancelled' THEN 'キャンセル' ELSE '不明' -- 未定義ステータスへのフォールバック END AS status_label FROM orders AS o INNER JOIN users AS u ON o.user_id = u.user_id ORDER BY o.order_id; /* 実行順序: 1. FROM orders AS o → 注文5行を読み込む 2. INNER JOIN users AS u → user_id で照合し user_name を付与 3. CASE o.status ... → 各行の status 値を評価し日本語ラベルに変換 4. SELECT / ORDER BY → 4列を order_id 昇順で出力 */
LEGEND
① FROM
FROM orders AS oorders テーブル(5行)を起点として読み込みます。status 列が CASE式の評価対象になります。| order_id | user_id | amount | status |
|---|---|---|---|
| 1 | 1 | 3000 | shipped |
| 2 | 1 | 1500 | pending |
| 3 | 2 | 5000 | delivered |
| 4 | 3 | 2000 | cancelled |
| 5 | 3 | 800 | pending |
o = orders(左) u = users(右)
CASE o.status WHEN '...' THEN '...' は単純CASE式で、指定した列の値と定数を = で比較します。一方 CASE WHEN o.amount > 5000 THEN '高額' WHEN o.amount > 1000 THEN '中額' ELSE '少額' END のような検索CASE式は任意の条件式が使えます。範囲・複合条件には検索 CASE を使います。SELECT 句だけでなく ORDER BY、GROUP BY、WHERE、集計関数の引数(例: COUNT(CASE WHEN status='shipped' THEN 1 END) で発送済みのみカウント)にも使えます。SQL の中で最も汎用的な式の一つです。NULL を返します。本番データには想定外のステータス値が入り込むことがあるため、必ず ELSE '不明'を書いてフォールバックを明示しましょう。派生テーブルJOIN — サブクエリで集計した結果(派生テーブル)とユーザーを結合する
派生テーブル(Derived Table)は、FROM句や JOIN 句の中に書いたサブクエリを一時的なテーブルとして扱う技法です。サブクエリにエイリアスを付けることで、通常のテーブルと同様に JOIN できます。
FROM users AS u INNER JOIN ( -- ここから派生テーブル(サブクエリ) SELECT user_id, MAX(ordered_at) AS latest, SUM(amount) AS total FROM orders GROUP BY user_id ) AS s -- 派生テーブルに別名 s を付ける ON u.user_id = s.user_id;
orders テーブルを user_id でグループ集計し(最新注文日・合計金額)、その結果を users テーブルと JOIN してユーザー名と一緒に取得してください。出力は最新注文日の新しい順で並べます。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1 | 1 | 3000 | 2024-01-10 |
| 2 | 1 | 1500 | 2024-02-15 |
| 3 | 2 | 5000 | 2024-01-20 |
| 4 | 3 | 2000 | 2024-03-05 |
| name | latest_order_date | total_amount |
|---|---|---|
| 山田 | 2024-03-05 | 2000 |
| 田中 | 2024-02-15 | 4500 |
| 佐藤 | 2024-01-20 | 5000 |
SELECT u.name, s.latest_order_date, s.total_amount FROM users AS u INNER JOIN ( -- FROM句のサブクエリ(派生テーブル) SELECT user_id, MAX(ordered_at) AS latest_order_date, -- 最新注文日 SUM(amount) AS total_amount -- 合計金額 FROM orders GROUP BY user_id ) AS s -- 派生テーブルに別名 s を付ける ON u.user_id = s.user_id ORDER BY s.latest_order_date DESC; -- 最新注文日の降順 /* 実行順序: 1. サブクエリ実行 → orders を GROUP BY user_id で集計 派生テーブル s(3行)が生成される 2. FROM users AS u → users の3行を読み込む 3. INNER JOIN ... AS s → u.user_id と s.user_id を照合・結合(3行保持) 4. SELECT u.name, s.* → 必要な3列を射影 5. ORDER BY latest_order_date DESC → 最新注文日の降順でソート */
LEGEND
① サブクエリ: FROM orders
FROM ordersまず FROM句内のサブクエリが実行されます。対象となる orders テーブル(4行)を読み込みます。| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1 | 1 | 3000 | 2024-01-10 |
| 2 | 1 | 1500 | 2024-02-15 |
| 3 | 2 | 5000 | 2024-01-20 |
| 4 | 3 | 2000 | 2024-03-05 |
u = users s = 派生テーブル(orders 集計結果)
WITH 句(Common Table Expression)で書き直すと可読性が上がります。WITH s AS (SELECT user_id, MAX(ordered_at) ... FROM orders GROUP BY user_id) SELECT ... FROM users JOIN s ... の形です。CTE はサブクエリと同じ結果を返しますが、名前を付けて先頭に定義できるため長いクエリでも構造が追いやすく、複数箇所で参照もできます。SELECT u.name, (SELECT MAX(ordered_at) FROM orders WHERE user_id = u.user_id) AS latest ... という書き方(相関サブクエリ)は外部クエリの各行ごとにサブクエリを実行するため、users が100万行あれば100万回実行されます。派生テーブル(または CTE)を使えば集計は1回で済みます。INNER JOIN (SELECT ...) ON ... のようにサブクエリにエイリアスを付けないと、PostgreSQL・MySQL いずれもエラーになります。派生テーブルには必ず AS エイリアス名を付けてください。WITH 句(CTE)に書き換えると複雑なクエリでも読みやすくなります。さらに発展として、複数行の最新レコードを取得する LATERAL JOIN や ウィンドウ関数も実務クエリで頻出のパターンです。