SELF JOIN — 同一テーブルを2つの役割で結合し、階層関係を表現する
SELF JOIN(自己結合)は、同じテーブルを2つの異なるエイリアスで参照し、行同士を結合するテクニックです。
「社員と上司が同じ employees テーブルに存在し、manager_id が同テーブルの emp_id を参照する」組織階層は、その代表例です。
FROM employees AS e -- 「部下」側として参照 LEFT JOIN employees AS m -- 「上司」側として参照 ON e.manager_id = m.emp_id;
AS e(部下)と AS m(上司)のように必ず異なるエイリアスを付ける必要があります。エイリアスがないとDBはどちらのテーブルを指しているか判断できません。社員テーブル(employees)には、各社員の manager_id(直属上司の emp_id)が格納されています。
このテーブルを自己結合し、各社員名(employee)と、その直属上司の名前(manager)を一覧で取得してください。上司がいない社員(社長)も出力に含めてください。
| emp_id | name | manager_id |
|---|---|---|
| 1 | 田中 | NULL |
| 2 | 佐藤 | 1 |
| 3 | 山田 | 1 |
| 4 | 鈴木 | 2 |
| employee | manager |
|---|---|
| 田中 | NULL |
| 佐藤 | 田中 |
| 山田 | 田中 |
| 鈴木 | 佐藤 |
アンチJOIN — 「一度も購入していないユーザー」をLEFT JOIN + IS NULLで抽出する
「Aテーブルには存在するが、Bテーブルには存在しないレコード」を取得するパターンをアンチJOIN(Anti-JOIN)と呼びます。
LEFT JOIN で全件を残し、右テーブル側が NULL になった行だけをWHEREで絞り込む2段構えの手法です。
FROM users AS u LEFT JOIN purchases AS p ON u.user_id = p.user_id WHERE p.user_id IS NULL; -- 右テーブルにマッチがない行だけを残す
WHERE u.user_id NOT IN (SELECT user_id FROM purchases) でも同じ結果を得られますが、NOT IN は右リストに NULL が混入すると結果が空になるリスクがあります。アンチJOINパターンの方が安全で高速なため、実務では推奨されています。ユーザー一覧(users)と購入履歴(purchases)のテーブルがあります。
一度も購入をしたことがないユーザーの名前だけを取得してください。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| purchase_id | user_id | item |
|---|---|---|
| 1 | 1 | 商品A |
| 2 | 1 | 商品B |
| 3 | 2 | 商品C |
| name |
|---|
| 山田 |
集計JOIN — サブクエリで部門平均を先に計算し、個人給与と比較する
「各社員の給与が、自部門の平均より高いか低いか」を判定するには、一度集計してから個人データと結合するというアプローチが必要です。
サブクエリ(または後述のCTE)で GROUP BY 集計を先に行い、その結果テーブルをJOINすることで、個人行を残したまま集計値を横付けできます。
FROM employees AS e INNER JOIN ( SELECT dept_id, AVG(salary) AS avg_salary FROM employees GROUP BY dept_id ) AS da ON e.dept_id = da.dept_id;
WHERE salary > AVG(salary) は構文として無効です。WHERE は集計前の各行を評価するため、集計関数を使えません。「先に集計 → 結果テーブルをJOIN」が正しい設計です。社員テーブル(employees)に給与(salary)と部門ID(dept_id)が格納されています。
各社員の名前・給与・所属部門の平均給与(avg_salary)を一覧で取得してください。
| emp_id | name | dept_id | salary |
|---|---|---|---|
| 1 | 田中 | 10 | 500 |
| 2 | 佐藤 | 10 | 700 |
| 3 | 山田 | 20 | 600 |
| 4 | 鈴木 | 20 | 400 |
| name | salary | avg_salary |
|---|---|---|
| 田中 | 500 | 600 |
| 佐藤 | 700 | 600 |
| 山田 | 600 | 500 |
| 鈴木 | 400 | 500 |
3テーブル連続LEFT JOIN — ECサイトの注文・ユーザー・商品を一気に結合する
実務のWebアプリケーションでは、1つのクエリで3つ以上のテーブルを結合することが頻繁に発生します。
LEFT JOIN を連続して重ねる場合、常に直前までの結合結果が「左テーブル」になります。右側のテーブルにマッチがなくても行を落とさないことが、NULL保持のポイントです。
FROM orders AS o LEFT JOIN users AS u ON o.user_id = u.user_id LEFT JOIN order_items AS oi ON o.order_id = oi.order_id;
FROM に指定したテーブルが「最も保護される側」です。どの後続テーブルにマッチしなくても、元の行は必ず結果に残ります。注文テーブル(orders)、ユーザーテーブル(users)、注文明細テーブル(order_items)の3つがあります。
3テーブルを結合して、注文ID・ユーザー名・商品名・数量を一覧で取得してください。商品が登録されていない注文(order_id=3)も出力に含めること。
| order_id | user_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| item_id | order_id | product_name | qty |
|---|---|---|---|
| 1 | 1 | Tシャツ | 2 |
| 2 | 1 | ジーンズ | 1 |
| 3 | 2 | スニーカー | 1 |
| order_id | name | product_name | qty |
|---|---|---|---|
| 1 | 田中 | Tシャツ | 2 |
| 1 | 田中 | ジーンズ | 1 |
| 2 | 佐藤 | スニーカー | 1 |
| 3 | 山田 | NULL | NULL |
CTE + JOIN — WITH句で事前集計し、ユーザー情報と結合する現代的SQLパターン
CTE(Common Table Expression / 共通テーブル式)は、WITH 名前 AS (サブクエリ) の形でSQLの冒頭に「名前付きの仮想テーブル」を定義する構文です。
複雑なサブクエリをネストさせる代わりに、上から下へ読める構造で書けるためコードの可読性が飛躍的に向上します。
WITH cte_name AS ( SELECT ... -- 事前集計や整形処理 ) SELECT ... FROM main_table LEFT JOIN cte_name ON ...;
ユーザーテーブル(users)と注文テーブル(orders)があります。
WITH句を使ってユーザーごとの注文件数(order_count)と合計金額(total_amount)を集計し、ユーザー情報と結合して出力してください。
一度も注文していないユーザー(山田)も出力し、注文件数は 0、合計金額は NULL で表示すること。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 1 | 5000 |
| 3 | 2 | 2000 |
| name | order_count | total_amount |
|---|---|---|
| 田中 | 2 | 8000 |
| 佐藤 | 1 | 2000 |
| 山田 | 0 | NULL |