awesome-hacks
Docs

SQLのJOIN選択とパフォーマンス — FULL OUTER JOINを避けるべき理由

FULL OUTER JOINが遅い原因と、LEFT JOINへの置き換えパターンを理解する。JOINの種類と使い分けの判断基準も整理する

最終更新:2026/07/08

TL;DR

  • FULL OUTER JOIN は両テーブルの全行を結合するため、結果セットが膨らみやすく遅い
  • 多くの場合は LEFT JOIN で代替できる。まず LEFT JOIN で表現できないか考える
  • FULL OUTER JOIN が本当に必要なのは「どちら側にも存在しないレコードを両方向で探したい」場面のみ

JOINの種類おさらい

JOINの基本的な使い方は PostgreSQL入門 で扱っている。
ここでは全 JOIN 種別をパフォーマンス観点で比較するために簡潔に整理する。

graph LR
    subgraph joins ["JOIN の種類と取得範囲"]
        IJ["INNER JOIN<br/>両テーブルで一致した行のみ"]
        LJ["LEFT JOIN<br/>左テーブル全行 + 右は一致分"]
        RJ["RIGHT JOIN<br/>右テーブル全行 + 左は一致分"]
        FJ["FULL OUTER JOIN<br/>両テーブルの全行"]
    end
JOIN種類左テーブル(非マッチ)右テーブル(非マッチ)結果行数の目安
INNER JOIN含まない含まない少ない
LEFT JOIN含む含まない中程度
RIGHT JOIN含まない含む中程度
FULL OUTER JOIN含む含む最大(両側の全行)

FULL OUTER JOIN が遅い理由

FULL OUTER JOIN はどちらのテーブルにも一致しない行を 両側から 返す。
これがパフォーマンス上の問題を引き起こす主な理由は2つある。

理由1:結果セットが最大まで膨らむ

LEFT JOIN の結果行数は次の式で決まる。

LEFT JOIN の結果行数 = 一致したペアの数 + 左テーブルの非マッチ行数

非マッチの左行は必ず1行として残るため、LEFT JOIN の結果行数が左テーブルの行数を下回ることはない。
ただし「左テーブルの行数に近い」のは 1行あたりの一致がおおよそ1件程度の場合の目安にすぎない。1人のユーザーが複数の注文を持つような1対多の関係があると、一致ペアの数がその分増えるため、結果行数は左テーブルの行数より大きくなる(詳しくはPostgreSQL入門のJOIN結果の例を参照)。

FULL OUTER JOIN はここに 右テーブルの非マッチ行 をさらに加えるだけなので、1対多の有無に関係なく次の関係が常に成り立つ。

FULL OUTER JOIN の結果行数 = LEFT JOIN の結果行数 + 右テーブルの非マッチ行数

「FULL OUTER JOIN は必ず LEFT JOIN 以上の行数になる」というのは、この式からいつでも確実に言える。

users: 100万行  orders: 500万行(1ユーザー平均1件の注文と仮定した単純化した例)

LEFT JOIN の目安:      100万行(usersが軸、注文がないユーザーはNULL補完で1行)
FULL OUTER JOIN の目安: 600万行(LEFT JOINの結果 + ordersにのみ存在する非マッチ行)

後続の WHERE や集計処理のコストがそのまま増える。

理由2:インデックスを活かしにくい実行計画になる

INNER JOIN や LEFT JOIN はインデックスを使った Nested Loop Join(インデックス走査 × 結合)が使いやすい。
FULL OUTER JOIN は両側の非マッチ行を収集する必要があるため、DBオプティマイザが Hash JoinMerge Join を選びやすく、大量データのときにメモリ・CPU コストが増える。

sequenceDiagram
    participant Q as クエリ
    participant L as 左テーブル
    participant R as 右テーブル

    Note over Q,R: LEFT JOIN の実行イメージ
    Q->>L: 左テーブル全行を取得
    Q->>R: 各行に対してインデックスで右テーブルを検索
    R-->>Q: 一致した行(なければ NULL)

    Note over Q,R: FULL OUTER JOIN の実行イメージ
    Q->>L: 左テーブル全行を取得
    Q->>R: 右テーブル全行を取得(ハッシュテーブル構築)
    Q->>Q: 一致・非マッチを両側で収集して結合

LEFT JOIN への置き換えパターン

パターン1:単純に左テーブルを軸にする

最も多いケースは、どちらのテーブルを軸にするかを決めれば FULL OUTER JOIN が不要になる。

-- NG: FULL OUTER JOIN(右側の非マッチ行が実際には不要)
SELECT u.id, u.name, o.item_name, o.price
FROM users AS u
FULL OUTER JOIN orders AS o ON u.id = o.user_id;
-- OK: usersを軸にした LEFT JOIN
--     注文がないユーザーも含め、すべてのユーザーと紐づく注文を取得する
SELECT u.id, u.name, o.item_name, o.price
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id;

「どちらのテーブルを中心にデータを見たいか」を明確にすれば、大抵は LEFT JOIN で表現できる。

パターン2:非マッチ行を探したい場合

「どちらかに存在しない行を探したい」だけなら、LEFT JOIN + IS NULL で対応できる。

-- ordersに紐づかないusersと、usersに紐づかないordersを両方探したい場合

-- NG: FULL OUTER JOIN で全部取って後から絞る
SELECT u.id, u.name, o.id AS order_id
FROM users AS u
FULL OUTER JOIN orders AS o ON u.id = o.user_id
WHERE u.id IS NULL OR o.id IS NULL;
-- OK: 2つの LEFT JOIN を UNION ALL で結合する
--     それぞれの方向でインデックスが効く

-- 注文がないユーザー
SELECT u.id AS user_id, u.name, NULL AS order_id
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id
WHERE o.id IS NULL

UNION ALL

-- ユーザーが存在しない注文(孤立レコード)
SELECT o.user_id, NULL AS name, o.id AS order_id
FROM orders AS o
LEFT JOIN users AS u ON o.user_id = u.id
WHERE u.id IS NULL;

FULL OUTER JOIN が本当に必要な場面

以下のケースは FULL OUTER JOIN を使うのが適切。

場面理由
2つのデータソースを突き合わせて差分を確認したいどちら側に欠けているかを一度に把握したい
マスターデータとトランザクションデータの完全照合両テーブルのすべての行が必要
データ移行後の整合性チェック旧・新テーブルを全行比較する

これらは「本当に両側の全行が必要」なケース。日常的な一覧取得・集計クエリに FULL OUTER JOIN が出てきたら、LEFT JOIN で書き直せないか見直すサインと考えてよい。


よくある落とし穴

意図せず行が増殖する

FULL OUTER JOIN は両テーブルの非マッチ行を追加するため、1対多の関係があると 想定より多くの行が返る ことがある。

-- usersとordersのFULL OUTER JOIN
-- 1ユーザーが3件の注文を持っていると、そのユーザーは3行になる
-- + ordersに存在しないusers行 + usersに存在しないorders行 が加わる
SELECT u.id, o.id
FROM users AS u
FULL OUTER JOIN orders AS o ON u.id = o.user_id;

集計クエリ(SUM・COUNT)と組み合わせると NULL を含む行が集計に混入し、意図しない結果になることがある。


関連