SQL JOIN — サブクエリと結合パターンの応用

応用JOIN応用サブクエリWeb開発PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

JOIN + GROUP BY 集計 — INNER JOINで結合した後にCOUNT・SUMで顧客別売上を集計する

NRO
前提知識

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;
GROUP BY に SELECT の非集計列を全て列挙:SELECT c.name のように集計関数なしで使う列は全て GROUP BY に含めます。GROUP BY c.customer_id だけでは c.name を SELECT できません(PostgreSQL・標準SQL)。
問題

customers テーブルと orders テーブルを INNER JOIN して、各顧客の注文件数(order_count)と合計金額(total_amount)を取得してください。注文のない顧客(鈴木)は除外し、合計金額の降順で並べてください。

使用テーブル
▸ customers
customer_idnamecity
1田中東京
2佐藤大阪
3山田東京
4鈴木名古屋
▸ orders
order_idcustomer_idamount
115000
213000
327000
432000
期待出力
nameorder_counttotal_amount
田中28000
佐藤17000
山田12000
QUESTION 7

FULL OUTER JOIN — 左右どちらにしか存在しない行も全て取得し退職・在籍・入社を判定する

UOA
前提知識

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 で id を統合する:FULL JOIN では左右どちらかの id が NULL になりうるため、COALESCE(a.id, b.id) で「どちらか非NULL の id」を取得するのが定番イディオムです。
MySQL は FULL OUTER JOIN 非対応:PostgreSQL・SQL Server・Oracle は標準サポート。MySQL では LEFT JOIN UNION RIGHT JOIN で代替します。
問題

2023年と2024年の従業員テーブルを FULL OUTER JOIN して、全従業員の在籍状況(退職・在籍継続・入社)を判定してください。片方の emp_id が NULL かどうかで CASE WHEN を使って分類します。

使用テーブル
▸ employees_2023(前年)
emp_idname
1田中
2佐藤
3山田
▸ employees_2024(今年)
emp_idname
2佐藤
3山田
4鈴木
期待出力
emp_idname_2023name_2024status
1田中NULL退職
2佐藤佐藤在籍継続
3山田山田在籍継続
4NULL鈴木入社
QUESTION 8

多対多 JOIN(M:N)— 中間テーブルを2回 INNER JOIN してユーザーとタグを結びつける

N:
前提知識

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
中間テーブルは「橋渡し役」:user_tags テーブルは user_id と tag_id だけを持ち、users と tags を結びつける橋の役割を果たします。2つの外部キーの組み合わせが主キーになるのが典型的な設計です。
問題

usersuser_tags(中間テーブル)・tags の3テーブルを2回 INNER JOIN して、各ユーザーが持つタグ名の一覧を取得してください。user_id → tag_id の順に並べてください。

使用テーブル
▸ users
user_idname
1田中
2佐藤
3山田
▸ user_tags(中間テーブル)
user_idtag_id
11
12
22
23
31
▸ tags
tag_idtag_name
1Frontend
2Backend
3DevOps
期待出力
nametag_name
田中Frontend
田中Backend
佐藤Backend
佐藤DevOps
山田Frontend
QUESTION 9

CTE(WITH句)+ JOIN — 商品別売上集計をCTEで仮想テーブル化してからJOINで詳細を付与する

INR
前提知識

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;
CTEは「一時的な名前付きサブクエリ」:FROM句のインラインサブクエリをCTEで書き直すと読みやすくなります。同じCTEを複数回参照でき、複雑な集計クエリの整理に有効です(PostgreSQL・MySQL 8+・SQL Server等で対応)。
問題

order_items テーブルを CTE で商品別に集計し(合計数量・注文件数)、products テーブルと INNER JOIN して商品名・合計数量・注文件数・合計売上金額(price × total_qty)を取得してください。合計売上金額の降順で並べてください。

使用テーブル
▸ products
product_idnameprice
1ノートPC80000
2マウス3000
3キーボード8000
▸ order_items
item_idproduct_idquantity
112
225
323
431
511
期待出力
nametotal_qtyorder_counttotal_revenue
ノートPC32240000
マウス8224000
キーボード118000
QUESTION 10

ROW_NUMBER() + サブクエリ JOIN — ウィンドウ関数でグループ内に連番を付け最新行1件だけ取得する

OA
前提知識

ウィンドウ関数 ROW_NUMBER() はグループ(PARTITION)ごとに行を並べて連番を振ります。この連番を使ってグループ内の特定行(最新1件・最大値など)のみをフィルタする手法は、実務で最も頻繁に登場する上級パターンの一つです。

ROW_NUMBER() OVER (
  PARTITION BY 列名    -- グループを定義(GROUP BY と異なり行を保持)
  ORDER BY     列名 DESC -- グループ内での並び順を定義
) AS rn              -- 最新・最大の行が rn=1 になる
ウィンドウ関数は GROUP BY と異なり行を消さない:GROUP BY は複数行を1行に集約しますが、ROW_NUMBER() は各行に連番を付けるだけで行数を変えません。PARTITION BY でグループを定義しても全行が保持され、グループ内での順位が rn として各行に付与されます。
WHERE でウィンドウ関数は直接使えない:WHERE ROW_NUMBER() OVER (...) = 1 と書くとエラーになります。ウィンドウ関数はサブクエリ(またはCTE)の中で計算し、外側のWHEREやJOIN ONでrnをフィルタするのが正しいパターンです。
問題

orders テーブルから各ユーザーの最新注文(ordered_at が最も新しい注文)を1件だけ取得して、users テーブルと INNER JOIN してユーザー名を付与してください。ウィンドウ関数でユーザー内の注文に新しい順の番号を付け、先頭行だけを抽出します。

使用テーブル
▸ users
user_idname
1田中
2佐藤
3山田
▸ orders
order_iduser_idamountordered_at
1150002024-01-10
2180002024-02-15
3230002024-01-20
4260002024-03-01
5340002024-02-05
期待出力
nameorder_idamountordered_at
佐藤460002024-03-01
田中280002024-02-15
山田540002024-02-05