【Oracle】MINUS で差分を取得する方法完全ガイド|NOT EXISTS との比較・NULL の扱い・EXCEPT・パフォーマンス・実務パターンまで解説

【Oracle】MINUS で差分を取得する方法完全ガイド|NOT EXISTS との比較・NULL の扱い・EXCEPT・パフォーマンス・実務パターンまで解説 Oracle

Oracle の MINUS 演算子は、1 つ目の SELECT の結果から 2 つ目の SELECT の結果を除外して差分を取得します。「テーブル A にはあるがテーブル B にはないデータ」を抽出する最もシンプルな方法です。

しかし「MINUS と NOT EXISTS はどう使い分けるのか」「NULL を含む行はどう扱われるのか」「大量データでのパフォーマンスはどうか」といった疑問もあります。

本記事では、MINUS の基本から、NOT EXISTS / NOT IN / LEFT JOIN との比較NULL の扱いOracle 21c の EXCEPT / MINUS ALL、そしてテーブル差分検出・マスタ不整合チェックの実務パターンまで体系的に解説します。

この記事でわかること
・MINUS 演算子の基本構文と動作(自動 DISTINCT)
・MINUS と NOT EXISTS / NOT IN / LEFT JOIN の違いとパフォーマンス比較
・MINUS での NULL の扱い(NULL 同士は等しいとみなされる)
・Oracle 21c の EXCEPT / MINUS ALL
・集合演算 3 種(UNION / INTERSECT / MINUS)の比較
・ORDER BY / 列数・データ型の注意点
・テーブル差分検出・マスタ不整合チェック・データ移行検証の実務パターン
スポンサーリンク

MINUS の基本構文と動作

SQL(MINUS 基本)
-- 構文
SELECT column1, column2 FROM table_a
MINUS
SELECT column1, column2 FROM table_b;
-- table_a にあって table_b にない行を返す
SQL(具体例)
-- テーブル A: employees_2024
-- emp_id: 1(Alice), 2(Bob), 3(Carol), 4(Dave)

-- テーブル B: employees_2025
-- emp_id: 1(Alice), 2(Bob), 5(Eve)

-- A MINUS B: A にあって B にない = 退職者
SELECT emp_id, emp_name FROM employees_2024
MINUS
SELECT emp_id, emp_name FROM employees_2025;
-- 結果: 3(Carol), 4(Dave)

-- B MINUS A: B にあって A にない = 新入社員
SELECT emp_id, emp_name FROM employees_2025
MINUS
SELECT emp_id, emp_name FROM employees_2024;
-- 結果: 5(Eve)
MINUS は自動的に DISTINCT を適用する
MINUS の結果には重複行が含まれません。1 つ目の SELECT に同じ行が複数あっても、結果は 1 行になります。これは UNION と同じ動作です。重複を保持したい場合は Oracle 21c の MINUS ALL を使います。
MINUS の順序で結果が変わる
A MINUS BB MINUS A は結果が異なります。・A MINUS B: A にあって B にない行
B MINUS A: B にあって A にない行
差分の方向を間違えるとデータが逆になるので注意してください。

MINUS vs NOT EXISTS vs NOT IN vs LEFT JOIN

「テーブル A にあるがテーブル B にないデータ」を取得する方法は MINUS 以外にもあります。それぞれの特徴を比較します。

SQL(4 つの書き方を比較)
-- (1) MINUS
SELECT emp_id FROM employees_a
MINUS
SELECT emp_id FROM employees_b;

-- (2) NOT EXISTS
SELECT a.emp_id FROM employees_a a
WHERE NOT EXISTS (
    SELECT 1 FROM employees_b b WHERE b.emp_id = a.emp_id
);

-- (3) NOT IN
SELECT emp_id FROM employees_a
WHERE emp_id NOT IN (
    SELECT emp_id FROM employees_b
);

-- (4) LEFT JOIN + IS NULL
SELECT a.emp_id
FROM employees_a a
LEFT JOIN employees_b b ON a.emp_id = b.emp_id
WHERE b.emp_id IS NULL;
方式 重複除去 NULL 安全 複数列比較 パフォーマンス 可読性
MINUS 自動(DISTINCT) 安全(NULL 同士=等値) 全列を自動比較 中〜大(ソート発生) 最もシンプル
NOT EXISTS なし(元の重複を保持) 安全 結合条件で指定 大量データで最速(インデックス活用) 普通
NOT IN なし 危険(NULL があると全行消える) 単一列のみ シンプル
LEFT JOIN IS NULL なし 安全 結合条件で指定 大(インデックス活用) やや冗長
NOT IN は NULL に注意
NOT IN (1, 2, NULL)全行が FALSE になり、結果が 0 行になります。これは x <> 1 AND x <> 2 AND x <> NULL と等価で、x <> NULL が常に UNKNOWN を返すためです。サブクエリの結果に NULL が含まれる可能性がある場合は、NOT EXISTS を使ってください。MINUS も NULL を安全に扱えます。
使い分けの判断基準
2 つの SELECT 全体の差分を手軽に確認MINUS(最もシンプル)
特定の列(主キーなど)で存在チェック + 他の列も取得NOT EXISTS(柔軟で高速)
大量データでパフォーマンス重視NOT EXISTS(インデックスが効きやすい)
重複行を保持したいNOT EXISTS または LEFT JOIN(MINUS は DISTINCT 強制)

MINUS と NULL の扱い

SQL(MINUS は NULL 同士を等しいとみなす)
-- テーブル A: (1, NULL), (2, 'X')
-- テーブル B: (1, NULL), (3, 'Y')

SELECT id, name FROM table_a
MINUS
SELECT id, name FROM table_b;
-- 結果: (2, 'X') のみ
-- (1, NULL) は両方にあるため除外される
-- → MINUS は NULL 同士を「等しい」として扱う
MINUS の NULL 扱いは = 演算子と異なる
通常の = 演算子では NULL = NULL は UNKNOWN(FALSE 扱い)ですが、MINUS(集合演算)では NULL 同士を等しいとして扱います。これは DISTINCT と同じ動作です。NULL を含むデータの差分比較では、MINUS が最も直感的な結果を返します。

EXCEPT と MINUS ALL(Oracle 21c 以降)

SQL(EXCEPT: SQL 標準の MINUS)
-- Oracle 21c 以降: EXCEPT が使える(MINUS と同じ動作)
SELECT emp_id FROM employees_a
EXCEPT
SELECT emp_id FROM employees_b;
-- MINUS と完全に同じ結果(SQL 標準準拠)
SQL(MINUS ALL / EXCEPT ALL: 重複を保持)
-- Oracle 21c 以降: 重複を保持した差分
-- テーブル A: (1), (1), (1), (2)
-- テーブル B: (1)

-- MINUS:     重複除去 → 結果: (2)
-- MINUS ALL: 重複保持 → 結果: (1), (1), (2)
--   A に (1) が 3 行、B に (1) が 1 行 → 差分 2 行 + (2) 1 行

SELECT id FROM table_a
MINUS ALL
SELECT id FROM table_b;
演算子 重複除去 Oracle バージョン SQL 標準
MINUS あり(DISTINCT 相当) 全バージョン Oracle 独自(21c 以降は EXCEPT も可)
EXCEPT あり(DISTINCT 相当) 21c 以降 SQL 標準
MINUS ALL なし(重複を保持) 21c 以降 Oracle 独自
EXCEPT ALL なし(重複を保持) 21c 以降 SQL 標準

集合演算 3 種の比較(UNION / INTERSECT / MINUS)

演算子 動作 数学的な意味
UNION 両方の結果を結合(重複除去) 和集合(A + B)
UNION ALL 両方の結果を結合(重複保持)
INTERSECT 両方に共通する行のみ 積集合(A かつ B)
MINUS A にあって B にない行のみ 差集合(A – B)
SQL(3 つの集合演算を同時に確認)
-- A = {1, 2, 3, 4}
-- B = {3, 4, 5, 6}

-- UNION:     {1, 2, 3, 4, 5, 6}
-- INTERSECT: {3, 4}
-- A MINUS B: {1, 2}
-- B MINUS A: {5, 6}

MINUS の使用ルールと注意点

ルール 説明 違反した場合
列数の一致 両方の SELECT の列数が同じでなければならない ORA-01789: query block has incorrect number of result columns
データ型の互換性 対応する列のデータ型が互換性を持つ必要がある ORA-01790: expression must have same datatype as corresponding expression
ORDER BY の位置 ORDER BY は最後の SELECT の後にのみ記述可能 コンパイルエラー
列名は最初の SELECT 結果の列名は 1 つ目の SELECT の列名が使われる
SQL(ORDER BY の正しい書き方)
-- NG: 各 SELECT に ORDER BY を書くとエラー
-- SELECT emp_id FROM table_a ORDER BY emp_id  -- NG
-- MINUS
-- SELECT emp_id FROM table_b;

-- OK: 最後にまとめて ORDER BY
SELECT emp_id, emp_name FROM employees_a
MINUS
SELECT emp_id, emp_name FROM employees_b
ORDER BY emp_id;  -- MINUS の結果全体に適用

実務パターン集

パターン(1): 2 つのテーブルの差分レコードを検出

SQL(テーブル差分の検出)
-- 本番テーブルとバックアップテーブルの差分を検出
-- 本番にあってバックアップにないレコード(追加分)
SELECT * FROM employees
MINUS
SELECT * FROM employees_backup;

-- バックアップにあって本番にないレコード(削除分)
SELECT * FROM employees_backup
MINUS
SELECT * FROM employees;

パターン(2): マスタの不整合チェック

SQL(マスタ不整合の検出)
-- orders テーブルに存在する customer_id で
-- customers マスタに存在しないもの(孤立データ)
SELECT customer_id FROM orders
MINUS
SELECT customer_id FROM customers;

-- 逆: マスタにあるが注文が 1 件もない顧客
SELECT customer_id FROM customers
MINUS
SELECT customer_id FROM orders;

パターン(3): データ移行の検証

SQL(移行元と移行先の差分確認)
-- 移行元にあって移行先にないレコード = 移行漏れ
SELECT emp_id, emp_name, salary FROM source_db.employees@db_link
MINUS
SELECT emp_id, emp_name, salary FROM target_employees;

-- 件数が 0 なら移行完了
-- 件数がある場合は移行漏れを調査

パターン(4): 権限の差分確認

SQL(ユーザー間の権限差分)
-- USER_A に付与されていて USER_B に付与されていない権限
SELECT privilege, table_name FROM all_tab_privs WHERE grantee = 'USER_A'
MINUS
SELECT privilege, table_name FROM all_tab_privs WHERE grantee = 'USER_B';

パターン(5): 双方向差分を一度に取得

SQL(双方向差分を UNION ALL で統合)
-- A にあって B にない + B にあって A にない を一度に取得
SELECT 'A_ONLY' AS diff_type, emp_id, emp_name
FROM (
    SELECT emp_id, emp_name FROM employees_a
    MINUS
    SELECT emp_id, emp_name FROM employees_b
)
UNION ALL
SELECT 'B_ONLY' AS diff_type, emp_id, emp_name
FROM (
    SELECT emp_id, emp_name FROM employees_b
    MINUS
    SELECT emp_id, emp_name FROM employees_a
)
ORDER BY emp_id;

パフォーマンスの注意点

注意点 理由 対処法
MINUS は内部でソートが発生する 重複除去のために両方の結果をソートして比較する 大量データでは NOT EXISTS(インデックス活用)を検討
全列比較のコスト SELECT * で MINUS すると全列を比較するためコストが高い 必要な列(主キーなど)だけ SELECT して MINUS する
MINUS を 3 つ以上チェーンする 各 MINUS ごとにソートが発生しコストが累積 可能なら 1 回の NOT EXISTS に書き換える
SQL(パフォーマンス改善: 全列 vs 主キーのみ)
-- 遅い: 全列を比較
SELECT * FROM employees_a
MINUS
SELECT * FROM employees_b;

-- 速い: 主キーだけで差分を検出 → 必要なら JOIN で他の列を取得
SELECT a.*
FROM employees_a a
WHERE a.emp_id IN (
    SELECT emp_id FROM employees_a
    MINUS
    SELECT emp_id FROM employees_b
);

よくある質問

QMINUS と EXCEPT は何が違いますか?
A動作は全く同じです。MINUS は Oracle 独自の構文、EXCEPT は SQL 標準の構文です。Oracle 21c 以降では両方使えます。他の RDBMS(PostgreSQL, SQL Server, MySQL 8.0+)では EXCEPT のみサポートされるため、移植性を考慮する場合は EXCEPT を使うのが安全です。
QMINUS の結果に重複行は含まれますか?
Aいいえ。MINUS は自動的に DISTINCT(重複除去)を適用します。1 つ目の SELECT に同じ行が複数あっても、結果には 1 行だけ含まれます。重複を保持したい場合は Oracle 21c の MINUS ALL を使うか、NOT EXISTS に書き換えてください。
QMINUS で NULL を含む行はどう扱われますか?
AMINUS(集合演算)では NULL 同士を等しいとして扱います。両方の SELECT に (1, NULL) がある場合、差分として残りません。これは通常の = 演算子(NULL = NULL は UNKNOWN)とは異なる動作です。
QNOT IN で結果が 0 行になります。MINUS では正しく返ります
ANOT IN のサブクエリの結果に NULL が 1 行でも含まれると、全行が除外されて結果が 0 行になります。これは SQL の仕様です(x <> NULL は常に UNKNOWN)。MINUS は NULL を安全に扱えるため、NULL を含むデータでは MINUS を使ってください。または NOT EXISTS も NULL 安全です。
QMINUS の結果に ORDER BY を付けるには?
AORDER BY最後の SELECT の後に記述します。
SELECT ... FROM table_a
MINUS
SELECT ... FROM table_b
ORDER BY column;

各 SELECT の中に個別に ORDER BY を書くことはできません。
QMINUS と NOT EXISTS はどちらが速いですか?
Aデータ量とインデックスの有無によります。少量データでは MINUS の方がシンプルで十分高速です。大量データ(数十万行以上)では NOT EXISTS の方が高速なことが多いです。NOT EXISTS は結合キーのインデックスを活用できるのに対し、MINUS は両方の結果をソートして比較するためです。実行計画(EXPLAIN PLAN)で確認するのが確実です。

まとめ

MINUS 演算子の要点をまとめます。

やりたいこと 推奨方法
A にあって B にないデータを取得 SELECT … FROM A MINUS SELECT … FROM B
重複を保持した差分(21c 以降) MINUS ALL
他 RDBMS にも移植可能な書き方(21c 以降) EXCEPT
特定の列で差分チェック + 他の列も取得 NOT EXISTS(柔軟性が高い)
大量データでの差分検出 NOT EXISTS(インデックスが効きやすい)
双方向差分を一度に取得 (A MINUS B) UNION ALL (B MINUS A)
マスタの孤立データを検出 SELECT key FROM child MINUS SELECT key FROM master
データ移行の検証 SELECT * FROM source MINUS SELECT * FROM target(0 件なら完了)

UNION の使い方は「UNION を使って複数の結果を結合する方法」も併せて参照してください。