【SQL】NOT IN完全ガイド|基本構文・NULL問題の原因と対策・NOT EXISTS・LEFT JOIN比較・DELETE/UPDATE・複数列まで解説

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 ININ の否定で、指定したリストのいずれにも一致しない行を返します。IN が「リストのどれかに一致」なら、NOT IN は「リストのどれにも一致しない」です。

IN NOT IN
意味 リストのいずれかに一致する行 リストのいずれにも一致しない行
構文 WHERE 列 IN (値1, 値2, ...) WHERE 列 NOT IN (値1, 値2, ...)
NULL の扱い NULL を含むリストでも他の値はマッチする リストに NULL が 1 件でもあると全件 0 件になる(後述)
NOT IN の基本的な使い方
-- 営業・開発以外の部門の従業員を取得
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 件になる落とし穴があります(次節で詳述)。

サブクエリを使った NOT IN
-- 注文したことがない顧客を取得(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 件になることです。多くの開発者が踏んでしまう重要なバグの原因です。

NULL が含まれると全件 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(中村)✓
なぜ NULL があると全件 0 件になるのか(三値論理):
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 → 全行が除外されます。
NULL 問題の 3 つの対処法
-- 【対処法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 を使うのが安全です。

NOT IN と 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 が最も高速になることが多いです。

3 手法の比較(注文していない顧客を取得)
-- 手法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 あり 大規模・パフォーマンス重視
LEFT JOIN + IS NULL パターン(アンチジョイン)の詳細:
「片方のテーブルにしか存在しないデータ」を取得する 3 手法の詳しい解説と使い分けは片方のテーブルに存在しないデータを取得する方法で解説しています。

DELETE・UPDATE での NOT IN

NOT IN は SELECT だけでなく DELETE・UPDATE の WHERE 句でも同じように使えます。実行前に必ず SELECT で対象を確認してから実行してください。

DELETE での NOT IN
-- 注文のない顧客を削除(実行前に 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 での NOT IN
-- 注文のない顧客のステータスを「未注文」に更新
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 フィルタを忘れずに
DELETE/UPDATE の NOT IN は実行前に必ず SELECT で確認:
NOT IN で大量データを削除・更新する場合、NULL の混入で意図しない結果になることがあります。必ず同じ WHERE 条件で SELECT を実行し、件数と内容を確認してから DELETE/UPDATE を実行してください。トランザクション(BEGIN/COMMIT/ROLLBACK)を使って確認後にコミットするのが安全です。

複数列の NOT IN(行値コンストラクタ)

複数の列の組み合わせで NOT IN を使いたい場合、行値コンストラクタ(列1, 列2))を使う方法と、AND で組み合わせる方法があります。RDBMS のサポート状況に注意が必要です。

複数列の NOT IN
-- 行値コンストラクタ: (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'
);
行値コンストラクタの RDBMS サポート:

  • 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 IN でのパフォーマンス最適化
-- 大量データでは 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)

QNOT IN を使ったら期待した件数より少ない(0件の)結果になりました。なぜですか?
Aサブクエリまたは定数リストに NULL が含まれている可能性があります。NOT IN のリストに NULL が 1 件でもあると、x <> NULL = UNKNOWN となりすべての行が除外されます。サブクエリに WHERE 列 IS NOT NULL を追加するか、NOT EXISTS に書き換えてください。まずサブクエリ単体を実行して NULL が含まれているか確認しましょう。
QNOT IN と NOT EXISTS のどちらを使うべきですか?
A定数リスト(NULL がないと確実な場合)は NOT IN の方が短くて読みやすいです。サブクエリや NULL を含む可能性がある列には NOT EXISTS の方が安全です。パフォーマンス重視でデータ量が多い場合は LEFT JOIN + IS NULL(アンチジョイン)が最速になることが多いです。
QMySQL で DELETE + NOT IN のサブクエリが「同じテーブルを参照できない」エラーになります。
AMySQL では 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)を使う方法もあります。
QNOT IN のサブクエリが遅いです。どうすれば高速化できますか?
A①サブクエリが返す列(結合キー)にインデックスを作成する。②NOT EXISTS か LEFT JOIN + IS NULL に書き換えてみて EXPLAIN で比較する。③サブクエリを先に絞り込んで件数を減らす(WHERE 条件を追加)。まず EXPLAIN で実行計画を確認し、フルスキャン(type: ALL)になっていればインデックス追加を検討してください。
QNOT IN (SELECT ...)NOT EXISTS (SELECT 1 FROM ... WHERE ...) で結果が違います。
Aサブクエリが NULL を返している場合に結果が異なります。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)は一致しないデータを抽出する方法で体系的に解説しています。テーブル間の不在データ取得の詳細は片方のテーブルに存在しないデータを取得する方法を参照してください。