#02 SQLをちゃんと理解する

JOINの仕組みを理解する

JOIN はどうやって動いているか

テーブルを結合する JOIN? 複数のテーブルを結合して一つの結果セットにする操作。INNER JOIN(両方に存在する行のみ)・LEFT JOIN(左テーブルの全行 + 一致した右テーブル)などがある。 は、SQLの中で最もよく使われる機能のひとつです。しかし「なんとなく使えている」状態では、複雑なクエリを書いたときやパフォーマンス問題が起きたときに対処できません。まずは内部アルゴリズムを理解しましょう。


JOINの内部アルゴリズム

JOINの内部アルゴリズム比較 Nested Loop Join Table A row 1 → row 2 → row 3 → 外側ループ Table B row 1 row 2 row 3 内側ループ 計算量: O(N × M) インデックスあり: O(N × log M) 適したケース: ・小テーブル同士 ・内側テーブルにインデックスあり Hash Join Phase 1: Build(ハッシュテーブル構築) Table B id=1 id=2 id=3 Hash Table h(1) → row1 h(2) → row2 h(3) → row3 Phase 2: Probe(A の各行でハッシュ参照) Table A id=1,2,3 Result 一致行を出力 O(1) ルックアップ 計算量: O(N + M) メモリ: O(M) 大テーブル同士・インデックスなしに有効
図1: Nested Loop JoinとHash Joinの動作比較

Nested Loop Join(ネステッドループ結合)

最もシンプルなアルゴリズムです。外側テーブルの各行に対して、内側テーブルを全走査して一致する行を探します。

for each row in table_A:
    for each row in table_B:
        if join_condition is true:
            output (row_A, row_B)

小さいテーブル同士や、内側テーブルに適切なインデックスがある場合に効率的です。インデックスがある場合、内側の走査がO(log n)になります。

Hash Join(ハッシュ結合)

大きなテーブル同士の結合で使われます。まず一方のテーブルをハッシュテーブルに読み込み、もう一方を走査しながらハッシュテーブルを参照します。

-- Phase 1: Build
for each row in table_B:
    hash_table[join_key] = row

-- Phase 2: Probe
for each row in table_A:
    match = hash_table[row.join_key]
    if match exists:
        output (row, match)

インデックスがなくても高速で、大量データの結合に向いています。ただしハッシュテーブル生成のためのメモリが必要です。


結合タイプの全体像

JOIN タイプ比較 Table A Table B 結果に含まれる行 結果に含まれない行 INNER JOIN 一致 両方に一致する行のみ LEFT JOIN A全行 左(A)の全行 + Bの一致分 RIGHT JOIN B全行 右(B)の全行 + Aの一致分 FULL OUTER JOIN A と B の全行 一致しない側の列は NULL になります LEFT JOIN → 右側がNULL  RIGHT JOIN → 左側がNULL  FULL OUTER → どちらの側もNULLになり得る CROSS JOIN(デカルト積) ON 句なし。A の全行 × B の全行の組み合わせ A: 3行 × B: 4行 = 12行 SELF JOIN(自己結合) 同じテーブルに別名をつけて2回 JOIN 例: 社員と上司の関係(同一テーブル)
図2: 各結合タイプと返される行の範囲

説明のために次の2つのテーブルを使います。

-- ordersテーブル
| order_id | customer_id | amount |
|----------|-------------|--------|
| 1        | 101         | 3000   |
| 2        | 102         | 1500   |
| 3        | 999         | 800    |  -- 存在しない顧客

-- customersテーブル
| customer_id | name   |
|-------------|--------|
| 101         | 田中   |
| 102         | 佐藤   |
| 103         | 鈴木   |  -- 注文なし

INNER JOIN

INNER JOIN? 結合条件が一致する行だけを返すJOIN。両テーブルに対応するデータが存在する場合のみ結果に含まれる。最もよく使われる結合方法。 は両方のテーブルに一致するレコードだけを返します。最も一般的な結合です。

SELECT o.order_id, c.name, o.amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;
| order_id | name | amount |
|----------|------|--------|
| 1        | 田中 | 3000   |
| 2        | 佐藤 | 1500   |
-- order_id=3(顧客999)と customer_id=103(鈴木)は結果に含まれない

LEFT JOIN

LEFT JOIN? 左テーブルの全行を返し、右テーブルに一致するデータがない場合はNULLで補うJOIN。「注文がないユーザーも含めて一覧表示したい」といった用途に使う。 は左テーブルの全行と、右テーブルの一致した行を返します。一致しない場合は右側がNULLになります。

SELECT o.order_id, c.name, o.amount
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id;
| order_id | name | amount |
|----------|------|--------|
| 1        | 田中 | 3000   |
| 2        | 佐藤 | 1500   |
| 3        | NULL | 800    |  -- 顧客が存在しないのでNULL

RIGHT JOIN

RIGHT JOIN は右テーブルの全行を保持します。LEFT JOIN を使って LEFT / RIGHT を入れ替えたものと等価なので、実務では LEFT JOIN に統一するチームが多いです。

SELECT o.order_id, c.name, o.amount
FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.customer_id;
| order_id | name | amount |
|----------|------|--------|
| 1        | 田中 | 3000   |
| 2        | 佐藤 | 1500   |
| NULL     | 鈴木 | NULL   |  -- 注文なし

FULL OUTER JOIN

両テーブルの全行を返します。一致しない側はNULLになります。MySQLでは直接サポートされていないため、LEFT JOIN と RIGHT JOIN を UNION で組み合わせる必要があります。

-- PostgreSQL / SQL Server
SELECT o.order_id, c.name
FROM orders o
FULL OUTER JOIN customers c ON o.customer_id = c.customer_id;

-- MySQL での代替
SELECT o.order_id, c.name FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
UNION
SELECT o.order_id, c.name FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.customer_id;

CROSS JOIN(デカルト積)

ON 句を持たず、全行の組み合わせを生成します。行数は テーブルA行数 × テーブルB行数 になります。

SELECT a.color, b.size
FROM colors a
CROSS JOIN sizes b;
-- colors が3行、sizes が4行なら12行返る

カレンダーテーブルの生成や、テストデータの組み合わせ生成などに活用されます。

SELF JOIN(自己結合)

同じテーブルを2回 JOIN します。組織の階層構造(上司・部下の関係)や、同じテーブル内で行同士を比較したいときに使います。

-- 社員と上司を同じテーブルから取得
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;

ON と USING の違い

JOIN の結合条件を指定する方法は2つあります。

-- ON: 列名が異なっていても使える(一般的)
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;

-- USING: 両テーブルで列名が同じ場合に使える(簡潔)
SELECT * FROM orders
JOIN customers USING (customer_id);

USING を使うと結果の列に customer_id が1列だけ含まれます(ON の場合は両テーブル分の列が含まれます)。列名が統一されているならUSINGの方がすっきり書けます。


複数テーブルをJOINする際の注意点

3テーブル以上を結合するときは、結合の順序と行の増減に注意が必要です。

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

行の増加に注意: 1対多の関係でJOINすると行が増えます。1つの注文に3つの商品がある場合、その注文の行は3行になります。意図せず COUNT(*) の結果がズレることがあるので注意してください。

NULL の伝播: LEFT JOIN の後に INNER JOIN を続けると、LEFT JOIN で生成された NULL 行が INNER JOIN の条件を満たせず除外されることがあります。結合の順序を意識して設計しましょう。


まとめ

  • JOIN の内部アルゴリズムには Nested Loop Join と Hash Join がある
  • INNER JOIN: 両テーブルに一致する行のみ
  • LEFT JOIN: 左テーブルの全行を保持(一致しない右側はNULL)
  • FULL OUTER JOIN: 両テーブルの全行を保持
  • CROSS JOIN: デカルト積(全組み合わせ)
  • SELF JOIN: 同じテーブルを2回結合して階層や比較に使う
  • 複数JOIN時は行の増加と NULL の伝播に注意

次回は サブクエリ? SQL文の中に入れ子になった別のSELECT文。WHERE句・FROM句・SELECT句などで使用できる。「平均より多い注文をしたユーザー」のような複雑な条件に使う。 の種類と使い分けを解説します。