【Oracle】集合演算完全ガイド|UNION・UNION ALL・INTERSECT・MINUS の違いと使い分け・パフォーマンス・NULL の扱い・実務パターン

【Oracle】集合演算完全ガイド|UNION・UNION ALL・INTERSECT・MINUS の違いと使い分け・パフォーマンス・NULL の扱い・実務パターン Oracle

集合演算は2つ以上の SELECT 結果を「縦に結合」する SQL の機能です。Oracle では UNIONUNION ALLINTERSECTMINUS の4種類があり、それぞれ「和集合(重複あり/なし)」「積集合」「差集合」に対応します。

この記事でわかること

  • UNION・UNION ALL・INTERSECT・MINUS の動作の違いと使い分け
  • 列数・データ型の一致ルールと型が異なる場合の対処
  • ORDER BY をどこに書くか・列の参照方法
  • NULL が集合演算でどう扱われるか
  • UNION ALL を使うべき理由(パフォーマンス)
  • MINUS を NOT EXISTS で書き換える方法とパフォーマンス比較
  • 複数テーブルを縦に結合する実務パターン
スポンサーリンク

4種類の集合演算と動作の違い

演算子 意味 重複行の扱い ソート発生
UNION 和集合(重複除去) 重複を除去して返す あり(DISTINCT 相当)
UNION ALL 和集合(重複あり) 重複をそのまま全件返す なし(高速)
INTERSECT 積集合(共通部分) 両方の結果に含まれる行のみ あり
MINUS 差集合 左の結果に存在して右に存在しない行 あり

まず動作を図で確認します。集合 A = {1, 2, 3}、集合 B = {2, 3, 4} の場合:

  • A UNION B:{1, 2, 3, 4}(重複の 2・3 は1件)
  • A UNION ALL B:{1, 2, 3, 2, 3, 4}(重複もそのまま6件)
  • A INTERSECT B:{2, 3}(共通部分のみ)
  • A MINUS B:{1}(A にあって B にない行)

UNION と UNION ALL

UNION:重複を除去した和集合
-- 現在の employees と退職者テーブル(employees_archive)を合わせた全員リスト
-- 同一人物が両テーブルにいる場合は1行に重複除去
SELECT employee_id, last_name, '現役' AS status
FROM employees
UNION
SELECT employee_id, last_name, '退職' AS status
FROM employees_archive;

-- UNION は結果に SORT(DISTINCT 相当の処理)が発生するため重い
-- 重複が絶対に発生しない場合でも SORT が走るコストがある
UNION ALL:重複をそのまま保持(推奨)
-- 重複が発生しないことが確実な場合は UNION ALL を使う(SORT なし・高速)
-- 例: 部門 10〜30 と 40〜60 は重複しない条件
SELECT employee_id, last_name, salary FROM employees WHERE department_id BETWEEN 10 AND 30
UNION ALL
SELECT employee_id, last_name, salary FROM employees WHERE department_id BETWEEN 40 AND 60;

-- 月次サマリーを縦積みする(各月テーブルを結合)
SELECT '2024-01' AS month, SUM(amount) AS total FROM sales_2024_01
UNION ALL
SELECT '2024-02',           SUM(amount)          FROM sales_2024_02
UNION ALL
SELECT '2024-03',           SUM(amount)          FROM sales_2024_03
ORDER BY month;
原則として UNION より UNION ALL を使う
UNION は重複除去のためにソートが発生し、大量データでは顕著に遅くなります。「重複が存在しない」または「重複があっても問題ない」場合は必ず UNION ALL を使ってください。重複除去が必要な場合も、外側に SELECT DISTINCT を付けた UNION ALL と速度を比較してみることをお勧めします。

INTERSECT:両方の結果に含まれる行だけを返す

INTERSECT の基本と実務パターン
-- 2024年と2025年の両方に注文した顧客を抽出
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2024
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2025;
-- → どちらの年にも購入した常連顧客の customer_id のみ返す

-- 複数の条件を全て満たす行を求める(AND 条件の別表現)
-- "エンジニア部門かつ給与 70000 以上" の社員
SELECT employee_id FROM employees WHERE department_id IN (60, 90)
INTERSECT
SELECT employee_id FROM employees WHERE salary >= 70000;
-- ※ この例は WHERE で AND 結合すれば済むが
--    テーブルが異なる場合や複雑な条件で INTERSECT が役立つ

-- INTERSECT は重複除去済みで返す(同じ顧客が複数回マッチしても1行)
INTERSECT は JOIN で代替できる場合が多い
INTERSECT はソートが発生するうえ、列が多い場合は比較コストが高くなります。同一テーブルへの条件絞り込みには AND 条件、異なるテーブルの共通行抽出には INNER JOIN または EXISTS サブクエリの方がインデックスを活用しやすく、高速になるケースが多いです。

MINUS:左の結果から右の結果を除いた行を返す

MINUS の基本と NOT EXISTS との比較
-- 全社員のうち、転職歴(job_history)がない社員を抽出
SELECT employee_id, last_name FROM employees
MINUS
SELECT DISTINCT e.employee_id, e.last_name
FROM employees e
JOIN job_history jh ON e.employee_id = jh.employee_id;

-- 同じ結果を NOT EXISTS で書くと、インデックスを活用できて高速
SELECT employee_id, last_name
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM job_history jh
    WHERE jh.employee_id = e.employee_id
);

-- テーブル間のデータ差分チェック(本番 vs バックアップの突合)
-- 本番にあってバックアップに「ない」か「値が違う」行を検出
SELECT employee_id, last_name, salary FROM employees
MINUS
SELECT employee_id, last_name, salary FROM employees_backup;
-- → 差があれば行が返る。0件なら2テーブルは完全一致

集合演算の共通ルール

列数・データ型の一致

集合演算では全クエリの 列数が同じでなければなりません。データ型は厳密に一致していなくても暗黙変換が可能な場合は動作しますが、明示的に CASTTO_CHAR で揃えるのが安全です。

列数・型の不一致への対処
-- NG: 列数が異なる(ORA-01789: クエリ・ブロックの結果列数が正しくありません)
-- SELECT employee_id, last_name FROM employees
-- UNION
-- SELECT department_id FROM departments;   -- 列が1つ → エラー

-- OK: NULL やリテラルで列数を合わせる
SELECT employee_id AS id, last_name AS name, '社員' AS category FROM employees
UNION ALL
SELECT department_id,    department_name, '部門'               FROM departments;

-- 型が異なる列を CAST で明示的に合わせる
SELECT employee_id, TO_CHAR(hire_date, 'YYYY-MM-DD') AS date_or_val FROM employees
UNION ALL
SELECT department_id, TO_CHAR(location_id)                          FROM departments;
ORDER BY は最後の SELECT の後に一度だけ書く
-- 集合演算の ORDER BY は全体の末尾に一度だけ書く
SELECT employee_id, last_name, salary FROM employees WHERE department_id = 50
UNION ALL
SELECT employee_id, last_name, salary FROM employees WHERE department_id = 90
ORDER BY salary DESC;        -- 最後の SELECT の後に書く(全結果に適用される)

-- ORDER BY には列名またはポジション番号を使う
-- 列名は最初の SELECT の列名(または別名)を参照する
SELECT employee_id AS id, last_name AS name FROM employees WHERE department_id = 50
UNION ALL
SELECT employee_id,        last_name        FROM employees WHERE department_id = 90
ORDER BY name;   -- 最初の SELECT の別名 "name" を参照

-- NG: 各 SELECT に ORDER BY を書くことはできない
-- SELECT ... ORDER BY salary   -- ORA-00933: SQLコマンドが正しく終了されていません
-- UNION ALL
-- SELECT ...;
NULL は集合演算では「等しい」と扱われる
-- 通常の WHERE では NULL = NULL は UNKNOWN(一致しない)
-- しかし集合演算では NULL 同士を「同じ値」とみなして重複除去・比較する

-- 以下、t1 = {NULL, 1}、t2 = {NULL, 2} という想定で説明

-- UNION: NULL と NULL は「重複」と判定されて1行になる
SELECT NULL AS val FROM DUAL UNION SELECT NULL FROM DUAL;
-- 結果: NULL(重複除去で1行)
-- ※ WHERE val = NULL は常に UNKNOWN だが、UNION の重複チェックは別ルール

-- 実際のテーブルでの例
SELECT commission_pct FROM employees WHERE department_id = 80   -- NULL 複数あり
UNION
SELECT commission_pct FROM employees WHERE department_id = 90;  -- NULL あり
-- commission_pct の NULL は1行にまとめられて返る

-- INTERSECT: NULL 同士は「一致」と判定されて返る
SELECT NULL AS val FROM DUAL INTERSECT SELECT NULL FROM DUAL;
-- 結果: NULL(両方に NULL があるので共通行として返る)

-- MINUS: NULL 同士は「一致」と判定されて除去される
-- t1 が {NULL, 1}、t2 が {NULL, 2} のとき
-- t1 MINUS t2 = {1}(NULL は t2 にもあるので除去、1 は t2 にないので残る)
SELECT commission_pct FROM employees WHERE department_id = 80
MINUS
SELECT commission_pct FROM employees WHERE department_id = 80;
-- 完全に同じ SELECT の MINUS → 0件(NULLも含めて全行が除去される)

集合演算の優先順位と括弧での制御

Oracle では INTERSECTUNIONUNION ALLMINUS より優先されます。意図した順序で処理させるには括弧を使って明示的に制御します。

優先順位と括弧による制御
-- INTERSECT は UNION より先に評価される(数式の掛け算と足し算の関係に似ている)
SELECT a FROM t1
UNION                              -- (2)
SELECT a FROM t2
INTERSECT                          -- (1) INTERSECT が先
SELECT a FROM t3;
-- 評価順: (t2 INTERSECT t3) → その結果 UNION t1

-- 意図した評価順を強制するには括弧を使う
(SELECT a FROM t1 UNION SELECT a FROM t2)  -- (1) t1 UNION t2 を先に
INTERSECT
SELECT a FROM t3;                           -- (2) その後 INTERSECT

-- 実務では括弧を付けて意図を明示する方が可読性が高い

実務パターン:複数ソースを縦に積んでレポートする

月次・年次・全体の合計を1つの結果セットで出力する
-- 月次・四半期・年次合計を縦に積んで出力(ROLLUP の代替パターン)
-- ※ ROLLUP の方が簡潔だが、集計元が異なるテーブルを使う場合に有効
SELECT '月次' AS period, TO_CHAR(order_date, 'YYYY-MM') AS period_val,
       SUM(amount) AS total
FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2024
GROUP BY TO_CHAR(order_date, 'YYYY-MM')

UNION ALL

SELECT '年次', '2024年合計', SUM(amount)
FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2024

ORDER BY period, period_val;
異なるテーブルをひとつのリストに結合する(汎用ログ照会など)
-- 複数種別の操作履歴を1つの時系列で表示する
SELECT
    '注文' AS action_type,
    order_id AS action_id,
    customer_id,
    order_date AS action_date
FROM orders
WHERE customer_id = 101

UNION ALL

SELECT
    '返品',
    return_id,
    customer_id,
    return_date
FROM returns
WHERE customer_id = 101

UNION ALL

SELECT
    '問い合わせ',
    inquiry_id,
    customer_id,
    inquiry_date
FROM inquiries
WHERE customer_id = 101

ORDER BY action_date DESC;
-- 顧客 101 のすべてのアクションを時系列で確認できる

まとめ

  • UNION:重複除去あり・SORT 発生。重複が確実にない場合は UNION ALL を使う
  • UNION ALL:重複そのまま・SORT なし。原則これを使い、必要なときだけ UNION に変える
  • INTERSECT:共通行のみを返す。大量データでは EXISTSJOIN の方が高速なことが多い
  • MINUS:差集合。差分チェックに便利。パフォーマンスが問題になる場合は NOT EXISTS で代替する
  • NULL の扱い:集合演算内では NULL = NULL を「一致」とみなして重複除去・比較される(通常の WHERE とは異なる)
  • 優先順位:INTERSECT → UNION / UNION ALL / MINUS の順。括弧で明示するのが安全

差分チェックの代替として使える NOT EXISTSNOT IN の詳細はサブクエリ完全ガイドを、複数の集計結果を縦に積む ROLLUP / CUBE / GROUPING SETSROLLUP・CUBE・GROUPING SETS完全ガイドも参照してください。