NOT IN は SQL で「指定したリスト以外のデータを取得する」ときに使う演算子です。実務でよく使いますが、リストに NULL が 1 件でも含まれると全件 0 件になるという重大な落とし穴があります。
この記事では NOT IN の基本構文から、NULL 問題の原因(三値論理)と対処法・NOT EXISTS や LEFT JOIN との使い分け・DELETE/UPDATE での応用・複数列の NOT IN まで体系的に解説します。
-- employees テーブル(従業員) -- id | name | department | salary | manager_id -- 1 | 田中 太郎 | 営業 | 55000 | NULL -- 2 | 鈴木 花子 | 開発 | 70000 | 1 -- 3 | 高橋 一郎 | 営業 | 48000 | 1 -- 4 | 伊藤 美咲 | 開発 | 80000 | NULL -- 5 | 渡辺 健二 | 人事 | 52000 | 4 -- 6 | 中村 さくら| NULL | 65000 | 4 -- orders テーブル(注文) -- order_id | customer_id | status -- 1 | 1 | shipped -- 2 | 3 | shipped -- 3 | 1 | pending -- customers テーブル(顧客) -- id | name -- 1 | 田中 -- 2 | 鈴木 -- 3 | 高橋 -- 4 | 伊藤
NOT IN の基本構文と IN との対称性
NOT IN は IN の否定で、指定したリストのいずれにも一致しない行を返します。IN が「リストのどれかに一致」なら、NOT IN は「リストのどれにも一致しない」です。
IN |
NOT IN |
|
|---|---|---|
| 意味 | リストのいずれかに一致する行 | リストのいずれにも一致しない行 |
| 構文 | WHERE 列 IN (値1, 値2, ...) |
WHERE 列 NOT IN (値1, 値2, ...) |
| NULL の扱い | NULL を含むリストでも他の値はマッチする | リストに NULL が 1 件でもあると全件 0 件になる(後述) |
-- 営業・開発以外の部門の従業員を取得
SELECT id, name, department
FROM employees
WHERE department NOT IN ('営業', '開発');
-- 結果:
-- id=5: 渡辺 健二(人事)✓
-- ※ id=6 は department が NULL なので NOT IN でも除外される(後述)
-- IN と NOT IN の対称性(両方で全件カバーされる)
-- IN を取得
SELECT name FROM employees WHERE department IN ('営業', '開発');
-- NOT IN を取得
SELECT name FROM employees WHERE department NOT IN ('営業', '開発');
-- ← NULL の行は両方の結果に含まれない!
定数リストでの NOT IN
-- 特定ステータスを除く注文を取得
SELECT order_id, customer_id, status
FROM orders
WHERE status NOT IN ('canceled', 'returned');
-- 特定 ID を除く全行
SELECT id, name FROM customers
WHERE id NOT IN (1, 3);
-- 結果: id=2(鈴木), id=4(伊藤)
-- 文字列リスト(カンマ区切り)
SELECT id, name, department
FROM employees
WHERE department NOT IN ('営業', '開発', '人事');
-- 結果: 0件(すべての部門が IN 内)+ department が NULL の行も除外
-- ORDER BY との組み合わせ
SELECT id, name, salary
FROM employees
WHERE department NOT IN ('開発')
ORDER BY salary DESC;
サブクエリとの組み合わせ
NOT IN のリストをサブクエリで動的に生成することで、「別テーブルに存在しないデータ」を取得できます。ただしサブクエリが NULL を返すと全件 0 件になる落とし穴があります(次節で詳述)。
-- 注文したことがない顧客を取得(customers にいて orders にいない)
SELECT id, name
FROM customers
WHERE id NOT IN (
SELECT customer_id FROM orders
);
-- 結果: id=2(鈴木), id=4(伊藤)✓
-- shipped の注文がない顧客
SELECT id, name
FROM customers
WHERE id NOT IN (
SELECT customer_id FROM orders WHERE status = 'shipped'
);
-- 管理職(manager_id が参照している id)以外の従業員
SELECT id, name
FROM employees
WHERE id NOT IN (
SELECT manager_id FROM employees WHERE manager_id IS NOT NULL
-- ↑ IS NOT NULL が重要!なければ全件 0 件になる
);
NOT IN の NULL 問題(全件 0 件になる重大な落とし穴)
NOT IN の最大の落とし穴は、リストやサブクエリの結果に NULL が 1 件でも含まれると、全件 0 件になることです。多くの開発者が踏んでしまう重要なバグの原因です。
-- manager_id には NULL が含まれる(田中・伊藤は manager_id が NULL)
-- NG: IS NOT NULL を付けないと全件 0 件
SELECT id, name
FROM employees
WHERE id NOT IN (
SELECT manager_id FROM employees -- manager_id に NULL が含まれる
);
-- → 0件!(期待: NULL でない manager_id に含まれない人)
-- 確認: サブクエリが返す値(NULL を含む)
SELECT manager_id FROM employees;
-- NULL, 1, 1, NULL, 4, 4
-- ↑ NULL が 2 件含まれるため NOT IN 全体が壊れる
-- OK: IS NOT NULL でサブクエリの NULL を除外する
SELECT id, name
FROM employees
WHERE id NOT IN (
SELECT manager_id FROM employees WHERE manager_id IS NOT NULL
);
-- 結果: id=2(鈴木), id=3(高橋), id=5(渡辺), id=6(中村)✓
NOT IN は内部的に
x <> 値1 AND x <> 値2 AND ... と展開されます。SQL では x <> NULL は TRUE でも FALSE でもなく UNKNOWN になります。UNKNOWN を AND で組み合わせると全体が UNKNOWN(または FALSE)になり、WHERE 条件が成立しません。つまり
id NOT IN (1, NULL) は id <> 1 AND id <> NULL = id <> 1 AND UNKNOWN = UNKNOWN → 全行が除外されます。-- 【対処法1】サブクエリに IS NOT NULL を付ける(最もシンプル)
SELECT id, name FROM employees
WHERE id NOT IN (
SELECT manager_id FROM employees WHERE manager_id IS NOT NULL
);
-- 【対処法2】COALESCE で NULL を意味のない値に置換する
SELECT id, name FROM employees
WHERE id NOT IN (
SELECT COALESCE(manager_id, -1) FROM employees
-- manager_id が NULL の場合は -1 に変換(-1 は employees.id に存在しない値)
);
-- 【対処法3】NOT EXISTS に書き換える(NULL に強い・推奨)
SELECT e.id, e.name
FROM employees AS e
WHERE NOT EXISTS (
SELECT 1 FROM employees AS m
WHERE m.manager_id = e.id
-- ↑ NULL の行は WHERE m.manager_id = e.id が UNKNOWN → EXISTS が false → NOT EXISTS が true
-- → NULL の行は「存在しない」と判定され、正しく動作する
);
-- 対処法3 が最も安全(NULL が含まれていても正しく動作する)
| 対処法 | 方法 | メリット | デメリット |
|---|---|---|---|
| IS NOT NULL フィルタ | サブクエリに WHERE 列 IS NOT NULL |
シンプル・直感的 | フィルタを書き忘れると再バグになる |
| COALESCE 置換 | COALESCE(列, 存在しない値) |
NULL を明示的に別値に変換 | 置換値の選定が必要(重複しない値を選ぶ) |
| NOT EXISTS に変更 | 相関サブクエリで書き直す | NULL に強い・構造的に安全 | 記述が長くなる |
NOT IN と NOT EXISTS の使い分け
NOT IN と NOT EXISTS は同じ結果を返すことが多いですが、NULL の扱いとパフォーマンスが異なります。NULL を含む可能性がある列には NOT EXISTS を使うのが安全です。
-- 注文していない顧客を取得する(2つの書き方)
-- NOT IN(サブクエリの customer_id に NULL がない前提)
SELECT id, name
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
-- NOT EXISTS(NULL があっても安全)
SELECT c.id, c.name
FROM customers AS c
WHERE NOT EXISTS (
SELECT 1 FROM orders AS o WHERE o.customer_id = c.id
);
-- NOT IN と NOT EXISTS が異なる結果になるケース
-- orders.customer_id に NULL がある場合:
-- NOT IN → 全件 0 件(NULL のせい)
-- NOT EXISTS → 正しく「注文がない顧客」を返す
NOT IN |
NOT EXISTS |
|
|---|---|---|
| NULL の扱い | リストに NULL があると全件 0 件になる | NULL に影響されず正しく動作する |
| 記述のシンプルさ | 定数リストなら短く書ける | 相関サブクエリで少し長くなる |
| パフォーマンス(小規模) | ほぼ同等 | ほぼ同等 |
| パフォーマンス(大規模) | RDBMS によって最適化される | 相関サブクエリのため行ごとに実行(遅くなる場合あり) |
| 推奨場面 | NULL がないと確実な列・定数リスト | NULL を含む可能性がある列・安全第一の場合 |
NOT IN vs LEFT JOIN + IS NULL のパフォーマンス比較
「片方のテーブルに存在しないデータ」を取得する 3 手法のうち、データ量が多い場合は LEFT JOIN + IS NULL が最も高速になることが多いです。
-- 手法1: NOT IN
SELECT id, name
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL);
-- 手法2: NOT EXISTS(相関サブクエリ)
SELECT c.id, c.name
FROM customers AS c
WHERE NOT EXISTS (
SELECT 1 FROM orders AS o WHERE o.customer_id = c.id
);
-- 手法3: LEFT JOIN + IS NULL(アンチジョイン)
SELECT c.id, c.name
FROM customers AS c
LEFT JOIN orders AS o ON c.id = o.customer_id
WHERE o.customer_id IS NULL;
| NOT IN | NOT EXISTS | LEFT JOIN + IS NULL | |
|---|---|---|---|
| NULL 安全性 | △(IS NOT NULL が必要) | ◎(安全) | ◎(安全) |
| インデックス活用 | ○(サブクエリ列にインデックスがあれば) | △(相関サブクエリは行ごと実行) | ◎(JOIN 列にインデックスが効く) |
| 大量データでの速度 | △ | △〜×(行数が多いと遅い) | ◎(最も速いことが多い) |
| 可読性 | ◎(最もシンプル) | ○ | ○ |
| 推奨場面 | 小規模・NULL なし確定 | 中規模・NULL あり | 大規模・パフォーマンス重視 |
「片方のテーブルにしか存在しないデータ」を取得する 3 手法の詳しい解説と使い分けは片方のテーブルに存在しないデータを取得する方法で解説しています。
DELETE・UPDATE での NOT IN
NOT IN は SELECT だけでなく DELETE・UPDATE の WHERE 句でも同じように使えます。実行前に必ず SELECT で対象を確認してから実行してください。
-- 注文のない顧客を削除(実行前に SELECT で確認必須)
-- 確認
SELECT id, name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL);
-- 削除実行
DELETE FROM customers
WHERE id NOT IN (
SELECT customer_id FROM orders WHERE customer_id IS NOT NULL
);
-- 注意: MySQL では DELETE + サブクエリで同じテーブルを参照できない
-- NG (MySQL): DELETE FROM orders WHERE id NOT IN (SELECT id FROM orders WHERE status = 'shipped')
-- OK (MySQL): サブクエリを一段ネストして回避
DELETE FROM orders
WHERE id NOT IN (
SELECT id FROM (
SELECT id FROM orders WHERE status = 'shipped'
) AS tmp
);
-- PostgreSQL / SQL Server / Oracle では直接参照可能
DELETE FROM orders
WHERE id NOT IN (SELECT id FROM orders WHERE status = 'shipped');
-- 注文のない顧客のステータスを「未注文」に更新
UPDATE customers
SET status = 'inactive'
WHERE id NOT IN (
SELECT customer_id FROM orders WHERE customer_id IS NOT NULL
);
-- 特定部門以外の従業員の給与を 5% 増加
UPDATE employees
SET salary = salary * 1.05
WHERE department NOT IN ('開発', '人事');
-- UPDATE でも NULL が含まれると全件更新されない点に注意
-- IS NOT NULL フィルタを忘れずに
NOT IN で大量データを削除・更新する場合、NULL の混入で意図しない結果になることがあります。必ず同じ WHERE 条件で SELECT を実行し、件数と内容を確認してから DELETE/UPDATE を実行してください。トランザクション(BEGIN/COMMIT/ROLLBACK)を使って確認後にコミットするのが安全です。
複数列の NOT IN(行値コンストラクタ)
複数の列の組み合わせで NOT IN を使いたい場合、行値コンストラクタ((列1, 列2))を使う方法と、AND で組み合わせる方法があります。RDBMS のサポート状況に注意が必要です。
-- 行値コンストラクタ: (department, salary) の組み合わせで除外
-- (MySQL / PostgreSQL / Oracle 対応、SQL Server は非対応)
SELECT id, name, department, salary
FROM employees
WHERE (department, salary) NOT IN (
('営業', 55000),
('開発', 70000)
);
-- SQL Server 互換: AND で組み合わせる(すべての RDBMS で動作)
SELECT id, name, department, salary
FROM employees
WHERE NOT (department = '営業' AND salary = 55000)
AND NOT (department = '開発' AND salary = 70000);
-- サブクエリで複数列の NOT IN
-- ordered テーブルにある (customer_id, product_id) の組み合わせ以外を取得
SELECT c.id, c.name
FROM customers c
WHERE (c.id, c.default_product_id) NOT IN (
SELECT customer_id, product_id FROM orders WHERE status = 'shipped'
);
- MySQL 5.5+・PostgreSQL・Oracle:
(col1, col2) NOT IN (...)対応 - SQL Server: 行値コンストラクタ非対応 → AND NOT で代替
- 複数列の NOT IN でも NULL の問題は同様に発生する(各列について UNKNOWN になる)
実務でよく使う NOT IN パターン集
-- 全商品リストのうち、今月注文のない商品
SELECT p.id, p.product_name
FROM products AS p
WHERE p.id NOT IN (
SELECT DISTINCT product_id
FROM orders
WHERE order_date >= '2024-04-01'
AND order_date < '2024-05-01'
AND product_id IS NOT NULL -- NULL 対策を忘れずに
);
-- 全ユーザーのうち、過去 90 日間ログインしていないユーザー
SELECT id, username
FROM users
WHERE id NOT IN (
SELECT DISTINCT user_id FROM login_logs
WHERE login_at >= CURRENT_DATE - INTERVAL 90 DAY
AND user_id IS NOT NULL
);
-- 処理済みリストに含まれないレコードを処理対象に
SELECT id, data
FROM pending_tasks
WHERE id NOT IN (
SELECT task_id FROM processed_tasks
WHERE processed_at >= '2024-01-01'
AND task_id IS NOT NULL
)
ORDER BY id
LIMIT 100; -- バッチで 100 件ずつ処理
-- 特定のカテゴリを除いた商品(NOT IN + 定数リスト)
SELECT product_name, price
FROM products
WHERE category_id NOT IN (99, 100, 101) -- 廃止カテゴリを除外
ORDER BY price;
-- 大量データでは NOT EXISTS か LEFT JOIN + IS NULL を検討 -- EXPLAIN で実行計画を確認する EXPLAIN SELECT id, name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL); -- インデックスが効いているか確認(type = ALL はフルスキャン) -- orders.customer_id にインデックスがなければ追加 CREATE INDEX idx_orders_customer_id ON orders (customer_id); -- NOT IN のリストが定数の場合はインデックスが効きやすい -- サブクエリの場合はオプティマイザの判断による
よくある質問(FAQ)
NOT IN のリストに NULL が 1 件でもあると、x <> NULL = UNKNOWN となりすべての行が除外されます。サブクエリに WHERE 列 IS NOT NULL を追加するか、NOT EXISTS に書き換えてください。まずサブクエリ単体を実行して NULL が含まれているか確認しましょう。NOT IN の方が短くて読みやすいです。サブクエリや NULL を含む可能性がある列には NOT EXISTS の方が安全です。パフォーマンス重視でデータ量が多い場合は LEFT JOIN + IS NULL(アンチジョイン)が最速になることが多いです。DELETE FROM テーブル WHERE id NOT IN (SELECT id FROM 同じテーブル) が直接書けません。サブクエリをもう一段ネストして SELECT id FROM (SELECT id FROM テーブル WHERE ...) AS tmp とすることで回避できます。または DELETE + JOIN 構文(DELETE t1 FROM テーブル AS t1 LEFT JOIN ... WHERE t2.id IS NULL)を使う方法もあります。EXPLAIN で実行計画を確認し、フルスキャン(type: ALL)になっていればインデックス追加を検討してください。NOT IN (SELECT ...) と NOT EXISTS (SELECT 1 FROM ... WHERE ...) で結果が違います。NOT IN はリストに NULL があると全件 0 件、NOT EXISTS は NULL を含んでいても正しく動作します。また NOT IN はサブクエリ全体を先に評価する「非相関サブクエリ」、NOT EXISTS は行ごとに評価する「相関サブクエリ」という違いもあります。一般的に NOT EXISTS の方が NULL 安全で推奨されます。まとめ
| やりたいこと | 書き方・ポイント |
|---|---|
| リストのどれにも一致しない行を取得 | WHERE 列 NOT IN (値1, 値2, ...) |
| サブクエリで除外リストを動的生成 | WHERE 列 NOT IN (SELECT 列 FROM ...) |
| NULL 問題を防ぐ(最重要) | サブクエリに WHERE 列 IS NOT NULL を追加する |
| NULL があっても安全な代替 | NOT EXISTS (SELECT 1 FROM ... WHERE id = 外部.id) |
| 大量データで最速の除外 | LEFT JOIN + IS NULL(アンチジョイン) |
| DELETE/UPDATE での除外 | WHERE 句で同様に使える(MySQL は同じテーブルの場合注意) |
| 複数列の組み合わせで除外 | (col1, col2) NOT IN ((v1, v2), ...)(SQL Server は AND NOT で代替) |
NOT IN を含む NOT 条件全般(<>・NOT LIKE・NOT BETWEEN・IS NOT NULL・EXCEPT)は一致しないデータを抽出する方法で体系的に解説しています。テーブル間の不在データ取得の詳細は片方のテーブルに存在しないデータを取得する方法を参照してください。