INNER JOIN — 共通キーで2テーブルを結合し、両方に存在する行だけを取得する
JOIN とは、共通のキー列(例: user_id)をもとに2つのテーブルを横方向に結合する操作です。正規化されたDBでは「ユーザー情報は users テーブル」「注文情報は orders テーブル」と分割して保存するため、関連データを一緒に取得するには JOIN が必要です。
INNER JOIN は最もシンプルなJOINで、ON句で指定したキーが両テーブルに存在する行のみを結果に含めます。片方のテーブルにしかないキーの行は除外されます(積集合)。
SELECT 列名, ... FROM 左テーブル AS エイリアス INNER JOIN 右テーブル AS エイリアス ON 左テーブル.キー列 = 右テーブル.キー列;
AS o のように短縮名を付けると o.user_id と書け、「ordersテーブルの user_id」と明示できます。特に同名の列が複数テーブルにある場合はエイリアス修飾が必須です。orders(注文)テーブルと users(ユーザー)テーブルを user_id で結合し、order_id・ユーザー名(name)・amount(注文金額)を取得してください。
※ orders の user_id = 4 は users テーブルに存在しません。また users の user_id = 3(山田)は注文がありません。
| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 2 | 5000 |
| 3 | 4 | 1200 |
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | name | amount |
|---|---|---|
| 1 | 田中 | 3000 |
| 2 | 佐藤 | 5000 |
SELECT o.order_id, u.name, -- u. プレフィックスで「usersテーブルのname」と明示する o.amount FROM orders AS o -- orders を起点(左テーブル)にする INNER JOIN users AS u -- users と結合(右テーブル) ON o.user_id = u.user_id -- 照合キー: 両テーブルの user_id が一致する行のみ結合 ORDER BY o.order_id; /* 実行順序: 1. FROM orders AS o → orders テーブル 2. INNER JOIN users AS u → orders の各行に対し ON 条件を満たす users の行を照合・結合 3. SELECT o.order_id, ... → 結合された仮想テーブル */
LEGEND
① FROM
FROM orders AS oorders テーブル(3行)を読み込みます。user_id 列が JOIN のキーになります。この後、各行に対して users テーブルとの照合が行われます。| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 2 | 5000 |
| 3 | 4 | 1200 |
o = orders u = users
AS o でエイリアスを付けると o.user_id と書けます。本問のように両テーブルに user_id という同名列がある場合、修飾なしの SELECT user_id は「どちらの user_id か」が曖昧としてエラーになります(ambiguous column)。複数テーブルをJOINする際は常にエイリアス修飾を書く習慣をつけましょう。INNER JOIN users と書いて ON句を省略すると、左テーブルの全行 × 右テーブルの全行のすべての組み合わせが出力されます。本問なら3行 × 3行 = 9行が出力される重大な事故です。実行前に必ず ON句の確認を。FROM orders, users WHERE orders.user_id = users.user_id という書き方はSQL-89の古い構文です。JOIN条件とフィルタ条件が同じWHEREに混在して可読性が下がるため、現代のSQL(SQL-92以降)では INNER JOIN ... ON 構文を使うのが標準です。LEFT JOIN — 左テーブルの全行を保持し、右テーブルにマッチしない行はNULLで補完する
LEFT JOIN(左外部結合)は INNER JOIN と違い、左テーブル(FROM 側)の全行を必ず結果に含めます。右テーブルに ON 条件でマッチする行がない場合、右テーブルの全列が NULL で補完されて出力されます。
SELECT 列名, ... FROM 左テーブル AS u -- 全行が必ず出力される LEFT JOIN 右テーブル AS o ON u.key = o.key; -- マッチしない場合、o.* は NULL
users テーブルの全ユーザーと、各ユーザーの注文情報(order_id・amount)を取得してください。注文が1件もないユーザーは order_id と amount を NULL で表示します。
※ 山田(user_id=3)は注文がありません。また、田中(user_id=1)は注文が2件あります。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| order_id | user_id | amount |
|---|---|---|
| 1 | 1 | 3000 |
| 2 | 1 | 1800 |
| 3 | 2 | 5000 |
| user_id | name | order_id | amount |
|---|---|---|---|
| 1 | 田中 | 1 | 3000 |
| 1 | 田中 | 2 | 1800 |
| 2 | 佐藤 | 3 | 5000 |
| 3 | 山田 | NULL | NULL |
SELECT u.user_id, u.name, o.order_id, -- 山田(user_id=3)はordersにマッチしないためこれらの列はNULLになる o.amount FROM users AS u -- 全ユーザー(左テーブル)を保持 LEFT JOIN orders AS o -- ordersはマッチする行のみ結合、なければNULL補完 ON u.user_id = o.user_id ORDER BY u.user_id, o.order_id NULLS LAST; /* 実行順序: 1. FROM users AS u → 左テーブルを読み込む 2. LEFT JOIN orders AS o → ON で照合(未マッチはNULL) 3. SELECT ... → 列を射影して出力 */
LEGEND
① FROM
FROM users AS uusers テーブル(3行)を左テーブルとして読み込みます。LEFT JOIN では、このテーブルの全行が結果に必ず含まれます。| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
u = users(左) o = orders(右)
NULL で補完されます。田中(user_id=1)は orders に2件あるため2行に展開され、山田(user_id=3)は NULL補完で1行になります。order_id = NULL は「0件」や「空文字」ではなく、orders テーブルに対応する行が存在しないことを示します。NULL に対して IS NULL / IS NOT NULL で条件をかけることができ、これが次問(Q3)の差集合パターンへつながります。LEFT JOIN orders ON ... WHERE o.amount > 0 のように RIGHT テーブルの列に条件をつけると、NULL はどの比較でも FALSE になるため NULL 行(山田)が除外されます。RIGHT テーブルへの条件は ON 句に書くか、意図が INNER JOIN なら INNER JOIN に変えましょう。LEFT JOIN + IS NULL — 一度も注文していないユーザーを「差集合」で抽出する
LEFT JOIN の特性(右テーブルにマッチしない行はNULL補完)を応用すると、「左テーブルにあって右テーブルにないレコード」、つまり差集合を取得できます。
FROM 左テーブル AS u LEFT JOIN 右テーブル AS o ON u.key = o.key WHERE o.key IS NULL -- NULLになった行 = 右テーブルにマッチしなかった行だけ残す
WHERE u.user_id NOT IN (SELECT user_id FROM orders) でも同じ結果になりますが、サブクエリが返すリストに NULL が1つでも含まれていると全件が空になるバグがあります(NULL との IN は全て UNKNOWN)。LEFT JOIN + IS NULL はこのバグがなく、大規模テーブルでも結合インデックスを使いやすいため実務で推奨されます。users テーブルのうち、orders テーブルに一件も注文が存在しないユーザー(user_id と name)を取得してください。
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| 4 | 鈴木 |
| order_id | user_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| user_id | name |
|---|---|
| 3 | 山田 |
| 4 | 鈴木 |
SELECT u.user_id, u.name FROM users AS u LEFT JOIN orders AS o -- まず全ユーザーと orders を LEFT JOIN する ON u.user_id = o.user_id -- orders にマッチしないユーザーは o.* が全て NULL になる WHERE o.order_id IS NULL -- NULL の行だけを残す = 「一度も注文していない」ユーザー ORDER BY u.user_id; /* 実行順序: 1. FROM users AS u → users を読み込む 2. LEFT JOIN orders AS o → ON で照合・NULL 補完 3. WHERE o.order_id IS NULL → 未マッチ行だけ通過 4. SELECT u.user_id, u.name → 2列を射影 */
LEGEND
① FROM
FROM users AS uusers テーブル(4行)を左テーブルとして読み込みます。| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
| 4 | 鈴木 |
u = users(左) o = orders(右)
WHERE u.user_id NOT IN (SELECT user_id FROM orders) でも同じ結果になりますが、サブクエリが返すリストに NULL が1つでも含まれると全件が空になるバグがあります(NULL との IN 比較は全て UNKNOWN となりフィルタを通過できない)。LEFT JOIN + IS NULL はこのバグがなく、インデックスも活用しやすいため実務で推奨されるパターンです。WHERE o.amount IS NULL のように JOIN キー以外の列で判定すると、「orders テーブルに amount = NULL で保存されている行」も誤ってマッチします。差集合パターンでは必ずJOIN キーの列(本問では o.order_id や o.user_id)で IS NULL 判定するのが正確です。3テーブルJOIN — order_items / orders / users を連鎖JOINで結合して注文明細を取得する
3つ以上のテーブルをJOINするには、JOINを連鎖させます。2回目のJOINは1回目のJOIN結果(仮想テーブル)に対して適用されます。
FROM order_items AS oi INNER JOIN orders AS o ON oi.order_id = o.order_id -- ① 1回目 INNER JOIN users AS u ON o.user_id = u.user_id; -- ② ①の仮想テーブルに追加JOIN
order_items → oi、orders → o、users → u)をエイリアスにするのが広く採用されている慣習です。複数JOINを含む長いSQLでは、エイリアスがないとどの列がどのテーブル由来か即座に判断できなくなります。ECサイトの注文明細(order_items)テーブルから、注文したユーザー名(user_name)・商品名(product_name)・数量(qty)を取得してください。
order_items → orders → users の順に INNER JOIN して結合します。
| item_id | order_id | product_name | qty |
|---|---|---|---|
| 1 | 1 | ノートPC | 2 |
| 2 | 1 | マウス | 1 |
| 3 | 2 | ノートPC | 3 |
| order_id | user_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| user_name | product_name | qty |
|---|---|---|
| 田中 | ノートPC | 2 |
| 田中 | マウス | 1 |
| 佐藤 | ノートPC | 3 |
SELECT u.name AS user_name, -- users テーブルの name 列 oi.product_name, -- order_items テーブルの商品名 oi.qty -- order_items テーブルの数量 FROM order_items AS oi -- 注文明細を起点 INNER JOIN orders AS o -- ① 注文明細と注文を order_id で結合 ON oi.order_id = o.order_id INNER JOIN users AS u -- ② ①の仮想テーブルに user_id でユーザーを結合 ON o.user_id = u.user_id ORDER BY oi.item_id; /* 実行順序: 1. FROM order_items AS oi → 注文明細3行を読み込む 2. INNER JOIN orders AS o → oi.order_id と o.order_id を照合・結合(3行保持) 中間テーブル: order_items + orders の情報が合流し、user_id 列が加わる 3. INNER JOIN users AS u → o.user_id と u.user_id を照合・結合(3行保持) 中間テーブル: さらに u.name が加わる(全テーブルの情報が揃う) 4. SELECT u.name, oi.product_name, oi.qty → 必要な3列を射影して出力 */
LEGEND
① FROM
FROM order_items AS oi注文明細テーブル(3行)を起点として読み込みます。order_id が次のJOINのキーになります。| item_id | order_id | product_name | qty |
|---|---|---|---|
| 1 | 1 | ノートPC | 2 |
| 2 | 1 | マウス | 1 |
| 3 | 2 | ノートPC | 3 |
oi = order_items o = orders u = users
INNER JOIN users は、order_items と orders を結合した仮想テーブルに対してさらに JOIN を実行します。3つでも4つでも「前のJOIN結果に次のテーブルをJOINする」という積み上げ方式は変わりません。最終的な仮想テーブルには全テーブルのカラムが揃い、SELECT 句でその中から必要な列だけを取り出します。order_items → oi、orders → o、users → u)をエイリアスに使うのが広く採用されている慣習です。複数JOINを含む長いSQLでは、エイリアスがないとどの列がどのテーブル由来かが即座にわからなくなります。SELECT name よりも SELECT u.name の方が、コードレビュー時に一目でわかります。SELECT COUNT(*) で行数チェックするデバッグ法が有効です。エイリアス.列名 でアクセスでき、最終的なSELECT句で必要な列だけを選んで出力します。JOIN + GROUP BY — JOINで仮想テーブルを作り、ユーザー別の注文件数・合計金額を集計する
JOIN と GROUP BY を組み合わせると、複数テーブルのデータを横断した集計クエリが書けます。SQL は FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY の順で評価されます。つまり JOIN で仮想テーブルを作ってから GROUP BY で集計するため、異なるテーブルの列を自由に集計キーや集計対象にできます。
FROM users AS u INNER JOIN orders AS o ON u.user_id = o.user_id -- JOINした仮想テーブルを GROUP BY で集計する GROUP BY u.user_id, u.name
users テーブルと orders テーブルを結合し、ユーザーごとの注文件数(order_count)と合計金額(total_amount)を集計してください。出力は合計金額の多い順(降順)で並べてください。
| 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 |
|---|---|---|
| 佐藤 | 1 | 5000 |
| 田中 | 2 | 4500 |
| 山田 | 2 | 2800 |
SELECT u.name, COUNT(o.order_id) AS order_count, -- NULL以外の値を数える(o.order_id は各注文に必ず存在する) SUM(o.amount) AS total_amount -- 注文金額の合計 FROM users AS u INNER JOIN orders AS o -- 注文のないユーザーはここで除外される(INNER JOINのため) ON u.user_id = o.user_id GROUP BY u.user_id, u.name -- SELECT の非集計列(u.name)を全部 GROUP BY に書く ORDER BY total_amount DESC; -- 集計後の列名で並べ替える /* 実行順序: 1. FROM users AS u → users の3行を読み込む 2. INNER JOIN orders AS o → orders と結合(5行の仮想テーブル) 3. GROUP BY u.user_id → ユーザーごとに3グループに分類 4. COUNT / SUM → 各グループ内で注文件数・合計金額を集計 5. SELECT u.name, ... → 集計結果の列を射影 6. ORDER BY total_amount DESC → 合計金額の降順でソート */
LEGEND
① FROM
FROM users AS uusers テーブル(3行)を左テーブルとして読み込みます。| user_id | name |
|---|---|
| 1 | 田中 |
| 2 | 佐藤 |
| 3 | 山田 |
u = users(左) o = orders(右)
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY の順に評価されます。JOIN で仮想テーブルを作ってから GROUP BY で集計するため、異なるテーブルの列を自由に集計キーや集計対象にできます。本問では u.name(users の列)でグループ化しながら o.amount(orders の列)を集計しています。SELECT u.name, COUNT(...) と書いた場合、u.name は集計関数を使っていない「非集計列」です。PostgreSQL・MySQL(strict mode)では SELECT 句の非集計列はすべて GROUP BY 句に含めなければエラーになります。本問では u.user_id でグループ化すれば u.name も一意に決まりますが、明示的に両方書くのが安全です。LEFT JOIN を使い、COUNT(o.order_id)(NULL をカウントしない)とすることで、注文ゼロのユーザーは order_count = 0 として出力されます。