SQLを使ってデータベースからデータを抽出する際、特定の条件に合致するデータを除外したり、2つの集合の差分を取得したりする操作は非常に重要です。
ビジネスの現場では、例えば「商品を購入したことがない顧客リスト」や「昨月のデータには存在するが今月は消えているレコード」を特定する場面が多くあります。
このような「特定の結果セットから別の結果セットを除く」操作を実現するのが、SQLの集合演算子であるEXCEPT句です。
本記事では、EXCEPT句の基本的な使い方から、類似した結果を得られるNOT INやNOT EXISTSとの違い、さらにはパフォーマンス面での使い分けについて詳しく解説します。
EXCEPT句とは:差集合を取得する集合演算子
EXCEPT句は、1つ目のクエリの結果から、2つ目のクエリの結果に含まれる行を取り除くために使用される集合演算子です。
数学の集合論における「差集合」に相当し、A集合からB集合との共通部分を差し引いた残りの要素を抽出します。
リレーショナルデータベースにおいて、複数のテーブル間の差分を確認する際に、直感的かつ簡潔に記述できるのが大きな特徴です。
EXCEPT句の基本構文
EXCEPT句の基本的な構文は以下の通りです。
-- 1つ目のSELECT文
SELECT 列名1, 列名2 FROM テーブルA
EXCEPT
-- 2つ目のSELECT文
SELECT 列名1, 列名2 FROM テーブルB;
このクエリを実行すると、テーブルAの結果セットから、テーブルBの結果セットと完全に一致する行が除外された結果が返されます。
注意点として、UNION句などの他の集合演算子と同様に、上下のSELECT文で選択する列の数とデータ型が一致している必要があります。
EXCEPT句の動作の特徴
EXCEPT句はデフォルトで、結果から重複行を排除する動作(DISTINCT相当)を行います。
つまり、1つ目のクエリに重複した行が含まれていたとしても、出力される結果セットではそれらは1つにまとめられます。
もし重複を保持したまま差分を取りたい場合は、一部のデータベース製品(PostgreSQLなど)ではEXCEPT ALLを使用することが可能です。
ただし、MySQLのようにEXCEPTをサポートしていないデータベースも存在するため、利用する製品の仕様を確認することが重要です。
実戦例:EXCEPT句で特定のユーザーを抽出する
具体的な活用シーンとして、顧客管理システムにおける「一度も注文をしたことがない顧客」の抽出を考えてみましょう。
ここでは、全顧客を管理するCustomersテーブルと、注文履歴を管理するOrdersテーブルを想定します。
-- 全顧客のIDを取得するクエリから、注文履歴がある顧客のIDを除く
SELECT CustomerID FROM Customers
EXCEPT
SELECT CustomerID FROM Orders;
このクエリを実行することで、注文実績のない顧客IDのリストを簡単に得ることができます。
実行結果のイメージは以下のようになります。
CustomerID
----------
1003
1005
1008
このように、集合演算として記述することで、何を行おうとしているのかがソースコードから読み取りやすいというメリットがあります。
NOT INとNOT EXISTS:除外のための別の選択肢
SQLで「除く」という操作を行う方法は、EXCEPT句だけではありません。
従来からよく使われている手法として、NOT IN句やNOT EXISTS句(相関副問合せ)があります。
これらは一見すると同じ結果を返しますが、NULL(空の値)の扱いやパフォーマンス特性において決定的な違いがあります。
NOT IN句による除外
NOT IN句は、指定したリストやサブクエリの結果に含まれない行を抽出します。
SELECT CustomerID
FROM Customers
WHERE CustomerID NOT IN (SELECT CustomerID FROM Orders);
この記述は非常に直感的ですが、大きな罠が潜んでいます。
もしサブクエリ(ここではOrdersテーブル)のCustomerIDに一つでもNULLが含まれていた場合、クエリの結果は常に空(0件)になってしまいます。
これはSQLの3値論理(TRUE, FALSE, UNKNOWN)に起因するものであり、意図しないバグを生む原因になりやすいため、NULLの可能性がある列に対してNOT INを使うのは避けるべきです。
NOT EXISTS句による除外
一方、NOT EXISTS句は、サブクエリ内で条件に合致する行が存在「しない」ことを判定します。
SELECT c.CustomerID
FROM Customers c
WHERE NOT EXISTS (
SELECT 1
FROM Orders o
WHERE o.CustomerID = c.CustomerID
);
NOT EXISTSはNULLの影響を受けず、期待通りの結果を確実に返します。
また、多くのデータベースエンジンにおいて、適切なインデックスが貼られていれば、大規模なデータに対しても高いパフォーマンスを発揮する傾向にあります。
EXCEPT vs NOT IN vs NOT EXISTS の比較
それぞれの手法には適した用途があります。
以下の表に主要な違いをまとめました。
| 手法 | 主な特徴 | NULLの扱い | 推奨される用途 |
|---|---|---|---|
| EXCEPT | 集合全体を比較。記述が簡潔。 | NULLを値として扱う | テーブル全体の差分を比較する場合 |
| NOT IN | 固定値リストとの比較に強い。 | 1つでもNULLがあると空になる | NULLが絶対にない定数リストとの比較 |
| NOT EXISTS | 行の存在有無を判定。高速。 | NULLの影響を受けない | 大規模データの結合・除外条件 |
EXCEPT句の特筆すべき点は、NULL自体を「1つの値」として認識し、比較対象に含めることができる点です。
例えば、両方のテーブルにNULLが含まれる行がある場合、EXCEPTはそれらを一致したものとして除外します。
これは、行全体の等価性をチェックしたい場合に非常に強力なツールとなります。
複数列での比較におけるEXCEPTの優位性
EXCEPT句の真価が発揮されるのは、比較したい列が複数ある場合です。
例えば、氏名とメールアドレスの両方が一致するデータを除外したいケースを考えます。
-- 顧客リストAから顧客リストBを完全に除外する
SELECT Name, Email FROM CustomerList_A
EXCEPT
SELECT Name, Email FROM CustomerList_B;
これをNOT EXISTSで書こうとすると、WHERE句に複数の結合条件を記述しなければならず、クエリが複雑化します。
EXCEPTを使用すれば、構造が同じ2つのデータセットをそのまま引き算するイメージで記述できるため、メンテナンス性が向上します。
データベース製品ごとの対応状況
SQL標準ではEXCEPTと定義されていますが、製品によってキーワードが異なる場合があります。
- PostgreSQL / SQL Server / SQLite:
EXCEPTを使用します。 - Oracle Database:
MINUSというキーワードを使用します。動作はEXCEPTとほぼ同じです。 - MySQL / MariaDB: 長らくサポートされていませんでしたが、MySQL 8.0.31 以降、およびMariaDB 10.3以降で利用可能になりました。
古いバージョンのMySQLを使用している環境では、LEFT JOINを使ってWHERE 右テーブル.キー IS NULLとする手法が一般的です。
パフォーマンスに関する考慮事項
EXCEPT句を使用する際は、背後で実行される「ソート」や「ハッシュ」の処理に注意が必要です。
一般的に、集合演算子は結果セット全体を一度メモリ上に展開し、重複を排除するための比較処理を行います。
そのため、数億件といった極めて巨大なデータセット同士を比較する場合、メモリ使用量が増大する可能性があります。
そのような場面では、適切なインデックスを貼った上でNOT EXISTSを利用する方が、オプティマイザによる最適化が効きやすく、実行速度が安定するケースが多いです。
クエリを作成した後は、必ず実行計画(EXPLAIN)を確認し、フルテーブルスキャンが発生していないかチェックする習慣をつけましょう。
まとめ
SQLで特定のデータを除外する「除く」操作には、複数のアプローチが存在します。
EXCEPT句(OracleではMINUS)は、集合演算として差分を取得する最もクリーンな方法であり、特に複数列の比較やNULLを含むデータの扱いに適しています。
一方で、実行効率やNULLトラップを考慮する必要がある場合は、NOT EXISTS句が強力な選択肢となります。
開発の初期段階では、可読性と意図の明確さを重視してEXCEPT句を検討するのが良いでしょう。
それぞれの演算子の特性を理解し、データの特性やパフォーマンス要件に応じて最適な手法を使い分けられるようになることが、SQLスキルの向上に繋がります。
