JOIN + GROUP BY 集計 — INNER JOINで結合した後にCOUNT・SUMで顧客別売上を集計する
JOIN と GROUP BY は「結合→グループ化→集計」という流れで組み合わせます。JOIN は行を横に広げ、GROUP BY は行を縦にまとめます。この2つを組み合わせることで、複数テーブルにまたがった集計クエリが書けます。
SELECT c.name, COUNT(o.order_id) AS order_count, -- 注文件数を集計 SUM(o.amount) AS total_amount -- 合計金額を集計 FROM customers AS c INNER JOIN orders AS o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name ORDER BY total_amount DESC;
SELECT c.name のように集計関数なしで使う列は全て GROUP BY に含めます。GROUP BY c.customer_id だけでは c.name を SELECT できません(PostgreSQL・標準SQL)。customers テーブルと orders テーブルを INNER JOIN して、各顧客の注文件数(order_count)と合計金額(total_amount)を取得してください。注文のない顧客(鈴木)は除外し、合計金額の降順で並べてください。
| customer_id | name | city |
|---|---|---|
| 1 | 田中 | 東京 |
| 2 | 佐藤 | 大阪 |
| 3 | 山田 | 東京 |
| 4 | 鈴木 | 名古屋 |
| order_id | customer_id | amount |
|---|---|---|
| 1 | 1 | 5000 |
| 2 | 1 | 3000 |
| 3 | 2 | 7000 |
| 4 | 3 | 2000 |
| name | order_count | total_amount |
|---|---|---|
| 田中 | 2 | 8000 |
| 佐藤 | 1 | 7000 |
| 山田 | 1 | 2000 |
FULL OUTER JOIN — 左右どちらにしか存在しない行も全て取得し退職・在籍・入社を判定する
FULL OUTER JOIN(完全外部結合)は、左右のテーブルのどちらかにしか存在しない行も含めて全行を結合します。一致しない側の列は NULL で補完されます。
SELECT ... FROM table_a AS a FULL OUTER JOIN table_b AS b ON a.id = b.id; -- a.id IS NULL → B にしか存在しない行 -- b.id IS NULL → A にしか存在しない行 -- どちらも非NULL → 両方に存在する行
COALESCE(a.id, b.id) で「どちらか非NULL の id」を取得するのが定番イディオムです。LEFT JOIN UNION RIGHT JOIN で代替します。2023年と2024年の従業員テーブルを FULL OUTER JOIN して、全従業員の在籍状況(退職・在籍継続・入社)を判定してください。片方の emp_id が NULL かどうかで CASE WHEN を使って分類します。
| emp_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| emp_id | name |
|---|---|
| 2 | 佐藤 |
| 3 | 山田 |
| 4 | 鈴木 |
| emp_id | name_2023 | name_2024 | status |
|---|---|---|---|
| 1 | 田中 | NULL | 退職 |
| 2 | 佐藤 | 佐藤 | 在籍継続 |
| 3 | 山田 | 山田 | 在籍継続 |
| 4 | NULL | 鈴木 | 入社 |
多対多 JOIN(M:N)— 中間テーブルを2回 INNER JOIN してユーザーとタグを結びつける
Webアプリでは「1人のユーザーが複数のタグを持ち、1つのタグも複数のユーザーに付く」という多対多(M:N)関係が頻繁に登場します。RDBではこの関係を中間テーブル(ジャンクションテーブル)で表現します。
SELECT u.name, t.tag_name FROM users AS u INNER JOIN user_tags AS ut ON u.user_id = ut.user_id -- ① users → 中間テーブル INNER JOIN tags AS t ON ut.tag_id = t.tag_id; -- ② 中間テーブル → tags
users・user_tags(中間テーブル)・tags の3テーブルを2回 INNER JOIN して、各ユーザーが持つタグ名の一覧を取得してください。user_id → tag_id の順に並べてください。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| user_id | tag_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 2 | 3 |
| 3 | 1 |
| tag_id | tag_name |
|---|---|
| 1 | Frontend |
| 2 | Backend |
| 3 | DevOps |
| name | tag_name |
|---|---|
| 田中 | Frontend |
| 田中 | Backend |
| 佐藤 | Backend |
| 佐藤 | DevOps |
| 山田 | Frontend |
CTE(WITH句)+ JOIN — 商品別売上集計をCTEで仮想テーブル化してからJOINで詳細を付与する
CTE(Common Table Expression、WITH句)は、複雑なサブクエリに名前を付けて「仮想テーブル」として再利用できる機能です。クエリを複数のステップに分解して書けるため、可読性と保守性が大幅に向上します。
WITH cte_name AS ( -- サブクエリ(集計・フィルタ等) SELECT col1, SUM(col2) AS total FROM some_table GROUP BY col1 ) SELECT m.name, c.total FROM master_table AS m INNER JOIN cte_name AS c ON m.id = c.col1;
order_items テーブルを CTE で商品別に集計し(合計数量・注文件数)、products テーブルと INNER JOIN して商品名・合計数量・注文件数・合計売上金額(price × total_qty)を取得してください。合計売上金額の降順で並べてください。
| product_id | name | price |
|---|---|---|
| 1 | ノートPC | 80000 |
| 2 | マウス | 3000 |
| 3 | キーボード | 8000 |
| item_id | product_id | quantity |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 5 |
| 3 | 2 | 3 |
| 4 | 3 | 1 |
| 5 | 1 | 1 |
| name | total_qty | order_count | total_revenue |
|---|---|---|---|
| ノートPC | 3 | 2 | 240000 |
| マウス | 8 | 2 | 24000 |
| キーボード | 1 | 1 | 8000 |
ROW_NUMBER() + サブクエリ JOIN — ウィンドウ関数でグループ内に連番を付け最新行1件だけ取得する
ウィンドウ関数 ROW_NUMBER() はグループ(PARTITION)ごとに行を並べて連番を振ります。この連番を使ってグループ内の特定行(最新1件・最大値など)のみをフィルタする手法は、実務で最も頻繁に登場する上級パターンの一つです。
ROW_NUMBER() OVER ( PARTITION BY 列名 -- グループを定義(GROUP BY と異なり行を保持) ORDER BY 列名 DESC -- グループ内での並び順を定義 ) AS rn -- 最新・最大の行が rn=1 になる
WHERE ROW_NUMBER() OVER (...) = 1 と書くとエラーになります。ウィンドウ関数はサブクエリ(またはCTE)の中で計算し、外側のWHEREやJOIN ONでrnをフィルタするのが正しいパターンです。orders テーブルから各ユーザーの最新注文(ordered_at が最も新しい注文)を1件だけ取得して、users テーブルと INNER JOIN してユーザー名を付与してください。ウィンドウ関数でユーザー内の注文に新しい順の番号を付け、先頭行だけを抽出します。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1 | 1 | 5000 | 2024-01-10 |
| 2 | 1 | 8000 | 2024-02-15 |
| 3 | 2 | 3000 | 2024-01-20 |
| 4 | 2 | 6000 | 2024-03-01 |
| 5 | 3 | 4000 | 2024-02-05 |
| name | order_id | amount | ordered_at |
|---|---|---|---|
| 佐藤 | 4 | 6000 | 2024-03-01 |
| 田中 | 2 | 8000 | 2024-02-15 |
| 山田 | 5 | 4000 | 2024-02-05 |