【SQL】NULLを含むカラムでの条件分岐・フィルターの書き方|三値論理・COALESCE・NULLIF・JOIN完全ガイド

【SQL】NULLを含むカラムでの条件分岐・フィルターの書き方 SQL

SQLでNULLが絡む条件式を書くとき、「なぜかデータが取れない」「NOT INで期待した結果にならない」「JOINしたら件数が合わない」といった問題に悩まされることがよくあります。

原因の多くは三値論理(TRUE / FALSE / UNKNOWN)の動作を知らずにコードを書いていることです。本記事では、NULLを含む条件分岐・フィルターで必ず押さえておくべき知識と、現場で使える実践パターンをすべて解説します。

この記事でわかること

  • 三値論理(UNKNOWN)が引き起こすバグのパターン
  • AND / OR / NOT での UNKNOWN 伝播の完全な動作表
  • COALESCE / NULLIF / NVL / IFNULL の使い分けと実践パターン
  • NOT IN + NULL が機能しない理由と安全な代替手法
  • OUTER JOIN 後のNULLフィルターで INNER JOIN 相当を実現する方法
  • ウィンドウ関数の IGNORE NULLS(Oracle / SQL Server)
  • GROUP BY・HAVING・CASE WHEN での NULL の動作
スポンサーリンク

なぜ WHERE col = NULL は動かないのか

SQLを始めたばかりのころ、多くの人がこのミスをします。

NG:動かない書き方
-- NULLを比較演算子で比較してもヒットしない
SELECT * FROM users WHERE email = NULL;   -- 0件
SELECT * FROM users WHERE email != NULL;  -- 0件

なぜ0件になるのでしょうか。SQLには三値論理という仕組みがあり、比較結果は TRUE / FALSE だけでなく UNKNOWN になることがあります。

NULL = NULLの結果は TRUE でも FALSE でもなく UNKNOWN です。WHERE句はUNKNOWNを「条件を満たしていない」として扱うため、NULL同士の比較ではレコードが返らないのです。

OK:正しい書き方
-- NULLの判定には IS NULL / IS NOT NULL を使う
SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;

三値論理(UNKNOWN)の完全な動作表

NULLが絡む条件式を正しく書くために、UNKNOWN の伝播ルールを理解することが重要です。

AND 演算表

A B A AND B
TRUE TRUE TRUE
TRUE FALSE FALSE
TRUE UNKNOWN UNKNOWN
FALSE TRUE FALSE
FALSE FALSE FALSE
FALSE UNKNOWN FALSE
UNKNOWN TRUE UNKNOWN
UNKNOWN FALSE FALSE
UNKNOWN UNKNOWN UNKNOWN

OR 演算表

A B A OR B
TRUE TRUE TRUE
TRUE FALSE TRUE
TRUE UNKNOWN TRUE
FALSE TRUE TRUE
FALSE FALSE FALSE
FALSE UNKNOWN UNKNOWN
UNKNOWN TRUE TRUE
UNKNOWN FALSE UNKNOWN
UNKNOWN UNKNOWN UNKNOWN

NOT 演算表

A NOT A
TRUE FALSE
FALSE TRUE
UNKNOWN UNKNOWN
重要な法則
① AND:FALSE が1つでも全体が FALSE(UNKNOWNは伝播する)
② OR:TRUE が1つでも全体が TRUE(UNKNOWNは伝播する)
③ NOT NULL:UNKNOWN のまま(TRUE にはならない)
WHERE 句は TRUE の行だけを返す → UNKNOWN は「除外」として扱われる

AND / OR での UNKNOWN 伝播パターン

三値論理の動作を理解すると、複合条件でのNULL絡みのバグが見えてきます。

UNKNOWN が伝播する複合条件の例
-- scoreがNULLの行は UNKNOWN AND TRUE = UNKNOWN → 除外される
SELECT * FROM students
WHERE score >= 80 AND grade = 'A';

-- scoreがNULLでもORで救える(UNKNOWN OR TRUE = TRUE)
SELECT * FROM students
WHERE score >= 80 OR score IS NULL;

-- NOT (score < 60) はNULLをヒットさせない
-- NOT UNKNOWN = UNKNOWN のため
SELECT * FROM students
WHERE NOT (score < 60);  -- NULLのscoreは除外される

-- NULLを含む行を拾いたい場合は明示的に OR IS NULL
SELECT * FROM students
WHERE NOT (score < 60) OR score IS NULL;
NOT(否定)はNULLをヒットさせない
NOT (score < 60)は「60未満ではない」という意味ですが、scoreがNULLの行は
NOT UNKNOWN = UNKNOWNとなり、WHERE句に除外されます。
「60未満でもなくNULLでもない」ではなく「NULLも含めて取得したい」場合はOR score IS NULLを追加してください。

NOT IN + NULL の危険な落とし穴

実務で最も踏みやすいNULL罠がこれです。NOT INのリストにNULLが1件でも含まれると全件0件になります。

NOT IN のリストにNULLが含まれる場合
-- productsテーブル
-- id: 1, 2, 3, 4, 5

-- deleted_idsテーブルにNULLが含まれる場合
-- id: 2, 4, NULL

-- 「2と4以外のproductを取得したい」つもりが...
SELECT * FROM products
WHERE id NOT IN (SELECT id FROM deleted_ids);
-- → 結果: 0件 !! NULLが含まれるためすべてUNKNOWNになる

-- 内部的には以下の条件に展開される:
-- id NOT IN (2, 4, NULL)
-- = id <> 2 AND id <> 4 AND id <> NULL
-- = id <> 2 AND id <> 4 AND UNKNOWN
-- = UNKNOWN → 全行除外
安全な代替手法:NOT EXISTS を使う
-- NOT EXISTS はNULLの影響を受けない
SELECT * FROM products p
WHERE NOT EXISTS (
    SELECT 1 FROM deleted_ids d
    WHERE d.id = p.id
);

-- または WHERE NULLを除外した上でNOT IN
SELECT * FROM products
WHERE id NOT IN (
    SELECT id FROM deleted_ids
    WHERE id IS NOT NULL  -- NULLを除外してからNOT IN
);
NOT IN vs NOT EXISTS の使い分け
サブクエリ結果にNULLが入る可能性がある場合はNOT EXISTSが安全です。リテラル値のリストにNULLが入ることはないのでWHERE id NOT IN (1, 2, 3)は問題ありません。詳しくはNOT IN完全ガイドをご参照ください。

NULL補完関数の使い分け

NULLを別の値に置き換える関数は複数あります。DBMSによって使える関数が異なるため、違いを整理します。

関数 MySQL PostgreSQL Oracle SQL Server 複数の候補値
COALESCE(a, b, ...) ○(全DBMS標準)
IFNULL(a, b) × × × ×(2引数のみ)
NVL(a, b) × × × ×(Oracle専用)
NVL2(a, b, c) × × × ×(Oracle専用)
ISNULL(a, b) × × × ×(SS専用)
COALESCE:複数の候補から最初の非NULLを返す
-- 最初の非NULLを返す(全DBMS対応)
SELECT
    name,
    COALESCE(mobile_phone, home_phone, '連絡先未登録') AS contact
FROM customers;

-- WHERE句でCOALESCEを使う
-- discount_priceがNULLのときはlist_priceで比較
SELECT * FROM products
WHERE COALESCE(discount_price, list_price) < 5000;
IFNULL(MySQL)/ NVL(Oracle)/ ISNULL(SQL Server)
-- MySQL: IFNULL
SELECT name, IFNULL(score, 0) AS score FROM students;

-- Oracle: NVL
SELECT name, NVL(score, 0) AS score FROM students;

-- Oracle: NVL2(col, NULLでない場合の値, NULLの場合の値)
SELECT name, NVL2(score, '採点済み', '未採点') AS status FROM students;

-- SQL Server: ISNULL
SELECT name, ISNULL(score, 0) AS score FROM students;

NULLIF:特定の値をNULLに変換する

NULLIF(a, b)a = b のとき NULL を返し、それ以外は a をそのまま返す 関数です。「ゼロ除算の防止」や「空文字をNULLとして扱う」場面でよく使います。

NULLIF の代表的な使い方
-- ゼロ除算の防止(NULLIF で分母がゼロの場合はNULLに)
SELECT
    department,
    total_sales,
    ROUND(total_sales / NULLIF(target_sales, 0) * 100, 1) AS achievement_rate
FROM sales_summary;

-- 空文字をNULLとして扱う
SELECT
    name,
    NULLIF(remarks, '') AS remarks  -- 空文字→NULL
FROM customers;

-- COALESCE + NULLIF の組み合わせ
-- 空文字もNULLも「未入力」として表示
SELECT
    name,
    COALESCE(NULLIF(remarks, ''), '未入力') AS remarks
FROM customers;

CASE WHEN でのNULL条件分岐

CASE式でNULLを扱う際の注意点と実践パターンです。

CASE WHEN IS NULL パターン
-- 基本:NULLの有無で表示を切り替える
SELECT
    user_id,
    name,
    CASE
        WHEN email IS NULL THEN 'メール未登録'
        WHEN email LIKE '%@gmail.com' THEN 'Gmail'
        ELSE 'その他メール'
    END AS email_status
FROM users;

-- NG: CASE WHEN email = NULL ← 絶対にヒットしない
SELECT
    CASE WHEN email = NULL THEN '未登録'  -- 常にFALSE扱い
         ELSE email
    END
FROM users;  -- BAD: IS NULLを使うこと
複数カラムのNULL組み合わせ分岐
-- 必須項目の入力状況を分類
SELECT
    user_id,
    CASE
        WHEN email IS NOT NULL AND phone IS NOT NULL THEN '完全登録'
        WHEN email IS NOT NULL AND phone IS NULL     THEN 'メールのみ'
        WHEN email IS NULL     AND phone IS NOT NULL THEN '電話のみ'
        ELSE                                              '未登録'
    END AS registration_status
FROM users;
CASE 式の ELSE は暗黙的にNULLになる
-- ELSE を省略するとNULLが返る
SELECT
    CASE WHEN score >= 80 THEN '合格'
         WHEN score >= 60 THEN '補講'
         -- ELSE を書かなかった場合: NULL が返る
    END AS result
FROM students;

-- NULLにしたくない場合はELSEを明示
SELECT
    CASE WHEN score >= 80 THEN '合格'
         WHEN score >= 60 THEN '補講'
         ELSE '不合格'
    END AS result
FROM students;

OUTER JOIN とNULLフィルター

LEFT JOINを使うと、右テーブルに対応行がない場合はNULLが埋まります。このNULLを使ったフィルタリングは実務で頻繁に登場します。

LEFT JOIN + IS NULL で「Aにあって Bにない」を抽出
-- 注文のないcustomerを取得(ANTI JOINパターン)
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;  -- JOINに失敗→NULLの行のみ

-- 右テーブルの非NULL行のみ(INNER JOINと同等)
SELECT c.customer_id, c.name, o.order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NOT NULL;
ON句の条件 vs WHERE句の条件に注意
LEFT JOINでフィルターをON句に書くかWHERE句に書くかで結果が変わります。
ON o.status = 'confirmed':statusが非confimredの場合でもcustomerは残り、o.*がNULLになる
WHERE o.status = 'confirmed':INNER JOINと同等になり、statusが非confirmedのcustomerが除外される
ON句とWHERE句の違い
-- ON句のフィルター: LEFT JOINの結合条件として使う
-- → customerは全件残り、直近30日の注文がない場合はo.*がNULL
SELECT c.customer_id, c.name, o.order_date
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
    AND o.order_date >= CURRENT_DATE - INTERVAL 30 DAY;

-- WHERE句のフィルター: JOINの後でフィルター
-- → 直近30日に注文していないcustomerは除外される(INNER JOIN相当)
SELECT c.customer_id, c.name, o.order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL 30 DAY;

GROUP BY / HAVING でのNULLの扱い

GROUP BYはNULLを「1つのグループ」として扱います。集計関数は通常NULLを除外するため、意識しないと件数が合わなくなります。

GROUP BY:NULLは1グループとしてまとまる
-- NULLのdepartmentは「NULLグループ」として集計される
SELECT
    department,           -- NULLのdepartmentも1行として出る
    COUNT(*) AS total,    -- NULLのdepartmentの人数
    COUNT(score) AS scored_count,  -- NULLのscoreは除外
    AVG(score) AS avg_score        -- NULLのscoreは除外して平均
FROM employees
GROUP BY department;

-- NULLグループを除外したい場合はWHEREで先にフィルタ
SELECT department, COUNT(*)
FROM employees
WHERE department IS NOT NULL
GROUP BY department;
HAVING でNULL集計を除外
-- 平均スコアがNULL(全員スコア未入力)のグループを除外
SELECT department, AVG(score) AS avg_score
FROM employees
GROUP BY department
HAVING AVG(score) IS NOT NULL;

-- 「未入力0件のグループ」のみを抽出するパターン
SELECT department, COUNT(*) AS total, COUNT(score) AS scored
FROM employees
GROUP BY department
HAVING COUNT(*) = COUNT(score);  -- 全員スコアあり

ウィンドウ関数での IGNORE NULLS

Oracle と SQL Server では、LAST_VALUEFIRST_VALUELAGLEAD などのウィンドウ関数で IGNORE NULLS オプションを使えます。センサーデータや株価のような「欠損値を直前の値で補完する」用途で重宝します。

Oracle / SQL Server – IGNORE NULLS で前の値を引き継ぐ
-- センサーデータ(NULLは計測失敗)
-- Oracle: LAG ... IGNORE NULLS
SELECT
    measured_at,
    temperature,
    LAST_VALUE(temperature IGNORE NULLS)
        OVER (ORDER BY measured_at
              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        AS filled_temperature
FROM sensor_data
ORDER BY measured_at;

-- PostgreSQL には IGNORE NULLS がないため CTE で代替
WITH filled AS (
    SELECT
        measured_at,
        temperature,
        COUNT(temperature) OVER (ORDER BY measured_at) AS grp
    FROM sensor_data
)
SELECT
    measured_at,
    temperature,
    FIRST_VALUE(temperature) OVER (PARTITION BY grp ORDER BY measured_at)
        AS filled_temperature
FROM filled
ORDER BY measured_at;

実践パターン集

パターン1:フォーム入力の入力状況チェック

必須項目・任意項目の入力状況を1クエリで確認
SELECT
    COUNT(*)                          AS total_users,
    COUNT(email)                      AS email_filled,
    COUNT(phone)                      AS phone_filled,
    COUNT(CASE WHEN email IS NULL AND phone IS NULL THEN 1 END)
                                      AS both_missing,
    ROUND(100.0 * COUNT(email) / COUNT(*), 1)
                                      AS email_fill_rate
FROM users;

パターン2:データクレンジング(空文字・NULLの統一)

空文字をNULLに統一してからCOALESCEで補完
-- 空文字とNULLが混在するカラムを一括クレンジング
UPDATE users
SET
    phone   = NULLIF(TRIM(phone), ''),
    address = NULLIF(TRIM(address), '')
WHERE phone = '' OR address = '';

-- 表示時に空文字・NULLを両方「未登録」として扱う
SELECT
    name,
    COALESCE(NULLIF(TRIM(phone), ''), '未登録') AS phone
FROM users;

パターン3:NULLを含む比較で「不明も含める」か「除外する」かを制御

年齢条件でNULLを明示的にコントロール
-- 「年齢20歳以上」のユーザーを取得(NULLは除外)
SELECT * FROM users WHERE age >= 20;

-- 「年齢20歳以上 または 年齢不明」を取得
SELECT * FROM users WHERE age >= 20 OR age IS NULL;

-- 「年齢18〜65歳の範囲外 または 不明」を取得
SELECT * FROM users
WHERE NOT (age BETWEEN 18 AND 65) OR age IS NULL;

パターン4:集計レポートでNULLをゼロに揃える

月次レポートでNULLを0に変換
-- 売上サマリーで売上がなかった月をゼロ表示
SELECT
    m.month_str,
    COALESCE(s.total_sales, 0) AS total_sales,
    COALESCE(s.order_count, 0) AS order_count
FROM (
    -- 全月マスタ
    SELECT DATE_FORMAT(DATE_ADD('2024-01-01', INTERVAL n MONTH), '%Y-%m')
           AS month_str
    FROM (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3
          UNION SELECT 4 UNION SELECT 5 UNION SELECT 6
          UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
          UNION SELECT 10 UNION SELECT 11) nums
) m
LEFT JOIN (
    SELECT DATE_FORMAT(order_date, '%Y-%m') AS month_str,
           SUM(amount)  AS total_sales,
           COUNT(*)     AS order_count
    FROM orders
    WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
    GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) s ON m.month_str = s.month_str
ORDER BY m.month_str;

よくある質問

QCOALESCE と CASE WHEN IS NULL の違いは何ですか?
A結果は同じですが書き方が異なります。COALESCE(col, '代替値')はNULLを別の値に置き換える関数で簡潔に書けます。CASE WHEN col IS NULL THEN '代替値' ELSE col ENDはより柔軟で、NULLかどうかに加えて他の条件も組み合わせられます。単純な補完はCOALESCE、複雑な分岐はCASE WHENを使うのが一般的です。
QWHERE col != ‘値’ はNULLの行を取得しますか?
A取得しません。col != '値'はNULLに対してUNKNOWNを返し、WHERE句ではUNKNOWNは除外されます。NULLも含めて取得したい場合はWHERE (col != '値' OR col IS NULL)と明示的に書いてください。
QNULL同士を比較したら同じとみなしてほしいのですが?
A通常の=ではNULL = NULLはUNKNOWNです。「NULLを同じと見なす」にはIS NOT DISTINCT FROM(PostgreSQL)またはCASE WHEN a IS NULL AND b IS NULL THEN TRUE WHEN a = b THEN TRUE ELSE FALSE ENDのような書き方を使います。MySQLでは<=>(NULL安全等価演算子)が使えます。
QCASE式でELSEを書かないとどうなりますか?
AELSE省略時はNULLが返ります。意図しないNULLを避けるために、CASE式には原則としてELSEを明示する習慣をつけましょう。
QMySQL のNULL安全等価演算子(<=>)とは何ですか?
Aa <=> bはNULLを通常の値として等価比較します。NULL <=> NULLはTRUE、NULL <=> 1はFALSEになります。MySQLとMariaDB専用の演算子で、PostgreSQLではIS NOT DISTINCT FROM、OracleではDECODE(a, b, TRUE, FALSE)が近い動作をします。

まとめ

NULLを含む条件分岐・フィルターの要点を整理します。

場面 正しい書き方 NGパターン
NULLの検索 IS NULL = NULL
NULL除外 IS NOT NULL != NULL
NOT INのNULL対策 NOT EXISTSまたはWHERE id IS NOT NULLでサブクエリ除外 NOT IN(NULLが1件でも全件0件に)
NULLを別値に置換 COALESCE(col, 値) CASE WHEN col = NULL
特定値をNULLに変換 NULLIF(col, 値)
否定条件でNULLを含む NOT (条件) OR col IS NULL NOT (条件)(NULLが除外される)
LEFT JOIN後の絞り込み ON句 or WHERE句を意図に合わせて選択 意識せず混在させる

関連記事:IS NULL完全ガイド(NULL検索・集計・ORDER BY NULLS)IS NOT NULL完全ガイドNOT IN完全ガイドCASE WHEN完全ガイド