SQL JOIN — 複雑な結合パターンとテーブル変化の応用

応用JOIN応用複雑な結合Web開発PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

FULL OUTER JOIN — 双方向の欠損を許容して2つのデータセットを完全突合する

FULL OUTER JOINCOALESCEデータ突合双方向欠損
前提知識

LEFT JOIN は「左テーブルを全て残す」、RIGHT JOIN は「右テーブルを全て残す」のに対し、FULL OUTER JOIN は「どちらのテーブルの行も取りこぼさずに全て結果に残す」結合です。片方にしか存在しない行は、反対側が NULL で補完されます。

-- 結合タイプの比較(左: A={1,2,3}  右: B={1,3,4} の場合)
LEFT  JOIN → 1, 2, 3       (Bに無い2はB側がNULL)
RIGHT JOIN → 1, 3, 4       (Aに無い4はA側がNULL)
FULL  JOIN → 1, 2, 3, 4    (2はB側NULL、4はA側NULL)
COALESCE で両側の NULL を補完: FULL JOIN では双方にNULLが生じます。IDや名前など「どちら側にも存在し得る列」は COALESCE(lm.col, tm.col) と両側を書いてNULLを補完するのが定石です。
MySQL 非対応:MySQLはFULL OUTER JOINをサポートしていません。LEFT JOIN … UNION ALL … RIGHT JOIN WHERE A.id IS NULL で代替します。PostgreSQL・BigQuery・SQL Server等では利用可能です。
問題

ECサイトの先月(last_month)と今月(this_month)の商品別売上テーブルがあります。商品によって、先月のみ売れたもの・今月のみ売れたもの・両月売れたものが混在しています。

全商品を網羅して、先月の売上(last_amount)と今月の売上(this_amount)を横に並べてください。片方の月に存在しない商品は NULL となります。

使用テーブル
▸ last_month
product_idproduct_nameamount
1PC500
2マウス120
3キーボード280
▸ this_month
product_idproduct_nameamount
1PC630
3キーボード210
4モニター450
期待出力
product_idproduct_namelast_amountthis_amount
1PC500630
2マウス120NULL
3キーボード280210
4モニターNULL450
QUESTION 7

ANTI JOIN — LEFT JOIN + WHERE IS NULL で「存在しないこと」を条件にする

LEFT JOINANTI JOIN差集合NOT EXISTS
前提知識

「テーブルAにあるが、テーブルBにはない行」を取得する操作を ANTI JOIN(差集合)と呼びます。離脱ユーザー検出・未処理チケットの洗い出し・マスタに存在しないデータの特定など、実務で非常に頻出するパターンです。

実現方法は主に2つあります。

-- ① LEFT JOIN + WHERE IS NULL(本問で学ぶ手法)
SELECT a.* FROM A LEFT JOIN B ON a.id = b.id
WHERE b.id IS NULL;

-- ② NOT EXISTS(意図がより明確)
SELECT a.* FROM A
WHERE NOT EXISTS (SELECT 1 FROM B WHERE b.id = a.id);
NOT IN は使わない:WHERE a.id NOT IN (SELECT id FROM B) は、サブクエリ結果に NULL が1つでも含まれると全行が UNKNOWN になり、結果が空になります。ANTI JOINには LEFT JOIN か NOT EXISTS を使ってください。
問題

先月購入した顧客テーブル(last_buyers)と今月購入した顧客テーブル(this_buyers)があります。

先月は購入したが、今月は購入しなかった(離脱した)顧客の user_iduser_name を抽出してください。

使用テーブル
▸ last_buyers
user_iduser_name
1田中
2佐藤
3鈴木
4高橋
▸ this_buyers
user_id
1
3
期待出力
user_iduser_name
2佐藤
4高橋
QUESTION 8

多対多 (M:N) JOIN — 中間テーブルを2段階でたどってタグ検索を実装する

INNER JOIN多対多中間テーブルタグシステム
前提知識

「1つの記事には複数のタグが付く」「1つのタグは複数の記事に付く」というような多対多(M:N)の関係は、外部キーを2つ持つ中間テーブル(junction table)でモデリングされます。これはWebアプリのDB設計で最も頻出する構造の1つです。

-- 多対多の代表的なDB構造
articles      (article_id, title)       ← エンティティA
tags          (tag_id, tag_name)        ← エンティティB
article_tags  (article_id, tag_id)     ← 中間テーブル(橋渡し)

-- 取得パターン: articles → article_tags → tags と2段階でたどる
FROM articles AS a
INNER JOIN article_tags AS at ON a.article_id = at.article_id
INNER JOIN tags         AS t  ON at.tag_id    = t.tag_id
DISTINCT が必要な場面:記事に複数の対象タグがついている場合、同じ記事が複数行に展開されます。記事の一覧だけ取得したい場合は SELECT DISTINCT で重複を除去するのが定石です。
問題

記事テーブル(articles)、タグテーブル(tags)、および両者を紐付ける中間テーブル(article_tags)があります。

タグ名が 'React' のタグが付いた記事の article_idtitle を取得してください。

使用テーブル
▸ articles
article_idtitle
1ReactでSPAを作る
2SQL JOINを完全解説
3VueとReactを比較する
▸ tags
tag_idtag_name
1React
2SQL
3Vue
▸ article_tags(中間テーブル)
article_idtag_id
11
22
31
33
期待出力
article_idtitle
1ReactでSPAを作る
3VueとReactを比較する
QUESTION 9

CTE + JOIN — WITH句で仮想テーブルを段階的に構築してコホート分析を実装する

CTE / WITHLEFT JOINコホート分析多段階集計
前提知識

複雑な集計SQLを一つのSELECT文に詰め込むと、ネストしたサブクエリが積み重なって可読性・保守性が著しく低下します。WITH 句(CTE: Common Table Expression)を使うと、集計を段階的な名前付き仮想テーブルに分解し、それをメインクエリでJOINして使うことができます。

-- CTEの基本構文
WITH cte_name AS (
  SELECT ...          -- ここに集計クエリを書く
  FROM   source_table
  GROUP BY ...
)
SELECT *
FROM   main_table
LEFT JOIN cte_name ON ...;  -- CTEをテーブルとしてJOINできる
ON句に日付計算を書く:LEFT JOINのON句には等価条件だけでなく fo.first_order_date <= u.register_date + INTERVAL '30 days' のような計算式も書けます。WHEREでなくON句に書くことで、LEFT JOINの左側の行を消さずに条件を適用できます。
問題

ユーザーの登録情報(users)と注文履歴(orders)テーブルがあります。ユーザーは複数回注文することがあります。

登録月(コホート月)ごとに、登録から30日以内に初回購入したユーザー数(converted_users)と全体の登録者数(total_users)を集計してください。

使用テーブル
▸ users
user_idregister_date
12024-04-01
22024-04-20
32024-05-05
42024-05-10
▸ orders
order_iduser_idorder_date
10112024-04-10
10212024-06-01
10322024-07-01
10432024-05-15
期待出力
cohort_monthtotal_usersconverted_users
2024-0421
2024-0521
QUESTION 10

関係除算的 JOIN — 同一テーブルを複数回 JOIN して「AかつB」を満たす行を抽出する

INNER JOIN自己結合関係除算複数条件AND
前提知識

「商品Aと商品Bの両方を購入したことがあるユーザーを探せ」という問題は、単純な WHERE … IN では解けません。なぜなら1行は1購入しか表さないため、「1行が同時に商品AであるかつBでもある」という矛盾した条件になるからです。

解決策は2つあります。

-- ① 同一テーブルを2回 SELF JOIN して、p1=商品A と p2=商品B を「横に並べる」
FROM purchases AS p1
INNER JOIN purchases AS p2
  ON  p1.user_id = p2.user_id
WHERE p1.product_code = 'A' AND p2.product_code = 'B'

-- ② HAVING で集計してから絞り込む(別解・商品数が多い場合に有効)
SELECT user_id FROM purchases
WHERE  product_code IN ('A', 'B')
GROUP BY user_id
HAVING COUNT(DISTINCT product_code) = 2
関係除算(Relational Division):「指定した全ての項目を満たすグループを探す」操作は関係代数の「除算」に相当します。商品が3つ以上に増えた場合は、JOINを追加するかHAVING COUNT = 3 のように変えるだけで対応できます。
問題

ユーザーの購入履歴テーブル(purchases)があります。各行は「あるユーザーが特定の商品を1回購入した」という1レコードです。

商品コード 'A' と商品コード 'B' の両方を購入したことがあるユーザーの user_id を抽出してください。

使用テーブル
▸ purchases
user_idproduct_code
1A
1B
1C
2A
3B
4A
4B
期待出力
user_id
1
4