◀ 6.

SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER)

▶
この記事の要点
  • SQL JOIN は複数テーブルを結合する基本構文。主要 5 種は INNER / LEFT / RIGHT / FULL OUTER / CROSS
  • OUTER JOIN では ON 条件と WHERE 条件で結果が変わる重要ポイント
  • USING (col) は両表で同名の指定列を等号結合する簡略記法。SELECT *では指定したUSING列が1列にまとまり、他の同名列はそのまま残る
  • EXISTS / NOT EXISTS は SEMI / ANTI JOIN を表現。性能面で INNER/LEFT より良いこともある
  • Join Algorithm は Nested Loop / Hash Join / Sort-Merge Join。インデックス・統計次第で最適解が変わる

JOIN の基本

SQL の JOIN は、複数テーブルを条件に従って組み合わせ、1つの結果セットを得る構文です。FROM A JOIN B ON A.x = B.x は等号結合の例で、等号以外の条件も使えます。

Oracle Database 19cを軸に、他製品の例を比較として区別します。次の複数行VALUESはMySQL/PostgreSQL等の比較用で、19cにはそのまま使えません。19cでは INSERT INTO employees VALUES (1, 'Alice', 10); のように各行を単行INSERTに分け、両表の計6行を扱います。CREATE/INSERTは説明用データを作る変更操作です。後続の結果コメントはこの3行ずつの入力からの期待値で、実測ログやORDER BYのない表示順の保証ではありません。

-- サンプル
CREATE TABLE employees (id INT, name VARCHAR(50), dept_id INT);
CREATE TABLE departments (id INT, name VARCHAR(50));

INSERT INTO employees VALUES
  (1,'Alice',10), (2,'Bob',20), (3,'Carol',NULL);
INSERT INTO departments VALUES
  (10,'Sales'), (20,'Engineering'), (30,'Marketing');

5 種類の JOIN

JOIN 種類左にのみ存在両方に存在右にのみ存在
INNER JOIN−○−
LEFT [OUTER] JOIN○(右側 NULL)○−
RIGHT [OUTER] JOIN−○○(左側 NULL)
FULL OUTER JOIN○○○
CROSS JOIN全組合せ(デカルト積)

INNER JOIN

SELECT e.name, d.name AS dept
FROM   employees e
INNER  JOIN departments d ON e.dept_id = d.id;

-- 結果
-- Alice  | Sales
-- Bob    | Engineering
-- (Carol は dept_id=NULL なので除外、Marketing は employee なしで除外)

LEFT OUTER JOIN

SELECT e.name, d.name AS dept
FROM   employees e
LEFT   JOIN departments d ON e.dept_id = d.id;

-- 結果
-- Alice  | Sales
-- Bob    | Engineering
-- Carol  | NULL          ← 左に居る人は必ず出る

RIGHT OUTER JOIN

SELECT e.name, d.name AS dept
FROM   employees e
RIGHT  JOIN departments d ON e.dept_id = d.id;

-- 結果
-- Alice  | Sales
-- Bob    | Engineering
-- NULL   | Marketing     ← 右に居る部署は必ず出る

FULL OUTER JOIN

FULL OUTER JOINは一致した組合せと左右の未一致行を返します。MySQL 8.4にはこの構文がありません。下の旧比較枠後半のLEFT UNION RIGHTは投影結果の重複を消すため、一般にはFULL JOINの行多重度を保ちません。単にUNION ALLへ替えるだけでも一致行を二重に数えます。

次は同じ e.dept_id = d.id 条件に限定したMySQL向け訂正形です。左結合全行に右未一致行だけを足します。この等号で一致する行のe.dept_idは必ず非NULLなので、右側枝の未一致判定に使えます。条件を変える場合は判定列も見直します。

SELECT e.name, d.name AS dept
FROM employees e LEFT JOIN departments d ON e.dept_id = d.id
UNION ALL
SELECT e.name, d.name AS dept
FROM employees e RIGHT JOIN departments d ON e.dept_id = d.id
WHERE e.dept_id IS NULL;

以下は元の製品別比較枠です。前半のFULL構文と、後半の重複排除を伴う旧UNION例を区別してください。

-- PostgreSQL / Oracle / SQL Server
SELECT e.name, d.name AS dept
FROM   employees e
FULL   OUTER JOIN departments d ON e.dept_id = d.id;

-- MySQL は LEFT UNION RIGHT で代替
SELECT e.name, d.name FROM employees e LEFT JOIN departments d ON e.dept_id = d.id
UNION
SELECT e.name, d.name FROM employees e RIGHT JOIN departments d ON e.dept_id = d.id;

CROSS JOIN (デカルト積)

SELECT e.name, d.name FROM employees e CROSS JOIN departments d;
-- 3 employees × 3 departments = 9 行

-- 古典的記法(カンマ)でも同じ
SELECT e.name, d.name FROM employees e, departments d;

SELF JOIN

同じテーブルを自分自身と結合。階層 / 上司部下関係などで活躍。

次の例はemployeesにmanager_id列がある別の教材です。冒頭DDLにはmanager_idがないため、その表のままでは実行できません。

-- 社員と上司
SELECT e.name AS employee, m.name AS manager
FROM   employees e
LEFT   JOIN employees m ON e.manager_id = m.id;

USING 句

結合キーが両表で同じカラム名のとき、USING (col) で簡潔に等号結合できます。SELECT *でまとまるのはUSINGに指定した列だけで、他の同名列まで統合されません。SQL ServerはこのUSING構文に対応しません。

次の「両表にdept_idがある場合」という前提が必要です。冒頭departmentsの列はid/nameだけなので、そのままの表には使えません。冒頭の表なら既出の ON e.dept_id = d.id を使います。

-- 両表に dept_id がある場合
SELECT * FROM employees JOIN departments USING (dept_id);
-- ON e.dept_id = d.dept_id と同じだが
-- 結果に dept_id が 1 列だけ出る(ON だと 2 列)

NATURAL JOIN

両表で同名カラム全てを自動結合。意図しない結合が起きるため避けるのがベストプラクティス。

次の例を冒頭の定義で読むと共通列はid/nameで、dept_idとidの結合にはなりません。created_atのコメントは、両表にその列がある別の場合への注意です。

SELECT * FROM employees NATURAL JOIN departments;
-- 両表に created_at があると、それも結合キーに使われて事故る

複数 JOIN

次はorders/customers/order_items/productsの別教材で、参照する全列がある前提です。OracleのDATE型created_atなら境界は o.created_at >= DATE '2026-06-01' とし、下の文字列比較をNLS非依存の共通日時構文とは扱いません。

SELECT o.id, c.name AS customer, p.name AS product, oi.quantity
FROM   orders o
JOIN   customers c ON o.customer_id = c.id
JOIN   order_items oi ON oi.order_id = o.id
JOIN   products p ON oi.product_id = p.id
WHERE  o.created_at >= '2026-06-01';

SEMI JOIN / ANTI JOIN

「片方に存在するか / しないか」を判定して左表の行だけ返す JOIN。EXISTS / NOT EXISTS で表現。

下のLEFT JOIN版の o.id IS NULL は、一致したorders行のidが必ず非NULLという前提が必要です。idがNULLを許すなら一致行も拾います。掲載の o.customer_id = c.id という等号条件に限定すれば、訂正形は WHERE o.customer_id IS NULL です。

-- SEMI JOIN: 注文がある顧客
SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- ANTI JOIN: 注文が一度もない顧客
SELECT * FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- LEFT JOIN + IS NULL でも同じ表現可(ANTI JOIN)
SELECT c.*
FROM   customers c
LEFT   JOIN orders o ON o.customer_id = c.id
WHERE  o.id IS NULL;

ON と WHERE の違い (OUTER JOIN)

OUTER JOINでは、フィルタを ON に書くか WHERE に書くかで意味が変わります。下の d.active = 1 はNULL補完行をWHEREで落としますが、すべてのWHERE条件が外部結合を内部結合と同じにするわけではありません。通常のINNER JOINで同じ条件をON/WHERE間で移す場合とは区別します。

次の例はdepartmentsにactive列がある別教材です。冒頭DDLにはactiveがないため、そのままの表では実行できません。

-- 例: アクティブな部署のみを左結合したいが
-- 部署が無い社員は残したい

-- ✅ ON に書く: Carol(部署無し) は残る
SELECT e.name, d.name
FROM   employees e
LEFT   JOIN departments d
       ON e.dept_id = d.id AND d.active = 1;

-- ❌ WHERE に書く: NULL がフィルタで弾かれ Carol が消える
SELECT e.name, d.name
FROM   employees e
LEFT   JOIN departments d ON e.dept_id = d.id
WHERE  d.active = 1;       -- d.active が NULL の Carol は除外される

Join Algorithm (Optimizer の選択)

アルゴリズム得意な場面コストの確認点(固定計算量ではない)
Nested Loop Join外側の対象が少なく、内側を効率的に探索できる場合外側の件数×内側へのアクセス。索引の有無・一致件数で変わる
Hash Join等号結合で大量行を扱う場合など。索引がないことは必須条件ではない入力行数・メモリ・ディスクへの退避・出力件数で変わる
Sort-Merge Join順序付きの入力を利用できる場合など。必要ならソートするソートの要否・入力/出力件数・メモリで変わる
Index Nested Loop内側の索引を利用するNested Loop。PK以外も候補索引アクセスと表アクセス、一致する行数を確認

次は製品別の省略断片で、実行済みログではありません。PostgreSQLのEXPLAIN ANALYZEは対象文を実行します。MySQLの通常EXPLAINとは別です。OracleのEXPLAIN PLANは計画表へ書き込み、DBMS_XPLAN.DISPLAYはその計画を表示します。SQL ServerのSTATISTICS PROFILE ONは後続の実行に作用し、単独では計画を表示しません。

-- 実行計画の確認
EXPLAIN ANALYZE SELECT ...           -- PostgreSQL
EXPLAIN SELECT ...                   -- MySQL
EXPLAIN PLAN FOR SELECT ...; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);  -- Oracle
SET STATISTICS PROFILE ON;           -- SQL Server

性能のコツ

  • 結合キーのインデックスは候補。既存索引・選択性・更新コストと計画を確認し、一律に追加しない
  • SQLに表を書く順序だけでNLJの内外や速度は決まらない。結合条件の制約とオプティマイザが選んだ実際の順序を確認する
  • 絞込み位置は計画で確認する。WHEREに書けば物理的に先に処理されるとは限らず、外部結合ではONとの移動で結果も変わり得る
  • 不要な列を SELECT しない(カバーリングインデックス効果)
  • 統計情報の状態を確認する。収集方法は製品別で、Oracleの統計収集はDBMS_STATSを使う。ANALYZE/UPDATE STATISTICSを製品共通の自動対処にしない

注文・顧客・商品の別教材

以下はemployees/departmentsの例とは独立した列契約の教材です。ordersにはorder_id/customer_id/amount/product_id、customersにはid/name、productsにはid/product_nameがある前提で、「複数 JOIN」節のorder_items経由・products.nameの例とも混ぜません。SQLの操作分類はOracle SQLのDML、SQL全体の分類と主要RDBMSの方言は総合的なSQLの解説を参照してください。

注文と顧客のINNER JOIN

注文と顧客が条件に一致する組合せを選びます。

SELECT o.order_id, c.name, o.amount
FROM orders o
INNER JOIN customers c
  ON o.customer_id = c.id;

顧客を残すLEFT JOIN

顧客を左に置き、対応する注文がない顧客も残します。

SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o
  ON c.id = o.customer_id;

注文・顧客・商品の3表結合

この教材はorders.product_idからproducts.idへ直接結合し、product_nameを選びます。

SELECT o.order_id, c.name, p.product_name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products p ON o.product_id = p.id;

カンマ表記と明示INNER JOINの対照

次の2文は同じ列と等号条件を使った内部結合の対照です。条件をまとめて読みやすくするには明示JOINを使えますが、表記だけで速度の優劣は決まりません。行の抽出条件はWHERE句、結合結果を集約した後の条件はHAVING句で区別します。

SELECT o.order_id, c.name
FROM orders o, customers c
WHERE o.customer_id = c.id;

SELECT o.order_id, c.name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

FAQ

Q: RIGHT JOIN はいつ使う?
A: ほぼ使いません。FROM B RIGHT JOIN A は FROM A LEFT JOIN B と等価で、可読性のため LEFT に統一するのが慣習。

Q: INNER JOIN ON 1=1 は何?
A: 結合条件「常に真」= CROSS JOIN。デカルト積を作りたいときに書きますが、意図を明確にするため CROSS JOIN 構文を使う方が良いです。

Q: NULL は JOIN でどう扱われる?
A: 通常の等号条件では NULL = NULL はUNKNOWNで一致しません。NULL同士も一致させる要件なら、Oracleでは ON (a.k = b.k OR (a.k IS NULL AND b.k IS NULL)) のように明示できます。PostgreSQLの IS NOT DISTINCT FROM は別製品の比較例です。NVLで同じ代替値へ置換すると実在値との衝突や型変換があるため、一律の代案にはしません。

公式仕様の確認先:Oracle19c SELECT、MySQL8.4集合演算、PostgreSQL EXPLAIN(2026-10-28確認)。画面・設定・コードは対象版と各例の前提条件を照合してください。

子ページ

子ページはありません

同階層のページ
  1. SELECT文
  2. INSERT
  3. UPDATE文
  4. DELETE文
  5. WHERE句
  6. JOIN句
  7. 集合演算子
  8. インラインビュー
  9. 副問い合わせ (サブクエリ)

最近更新/作成されたページ