#07 SQLをちゃんと理解する

NULLを正しく扱う

NULL とは何か

NULL? SQLにおける「値が存在しない」を表す特殊な状態。NULLは値ではないため、`= NULL` では比較できず、`IS NULL` / `IS NOT NULL` を使う。NULLを含む演算は基本的にNULLになる。 は「値が存在しない」「不明」「適用不可」を表す特別な状態です。これは 0(数値のゼロ)や ''(空文字列)とは根本的に異なります。

-- NULLとゼロと空文字の違い
NULL  -- 値が存在しない(例: 電話番号が登録されていない)
0     -- 数値の0(例: 残高が0円)
''    -- 空の文字列(例: コメントが空欄で提出された)

この違いを理解していないと、検索や集計で意図しない結果が返ることがあります。


3値論理(TRUE / FALSE / UNKNOWN)

通常のプログラミング言語の論理値はTRUEかFALSEの2値です。しかしSQLでは NULL を含む比較の結果は UNKNOWN になります。これが「3値論理」です。

NULL演算の結果 — 3値論理 AND の真理値表 A AND B TRUE FALSE TRUE TRUE FALSE FALSE FALSE FALSE UNKNOWN UNKNOWN FALSE OR の真理値表 A OR B TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE UNKNOWN TRUE UNKNOWN NOT と算術演算 NOT TRUE = FALSE NOT FALSE = TRUE NOT UNKNOWN = UNKNOWN NULL + 1 = NULL NULL * 0 = NULL NULL || ''= NULL NULL を含む比較 NULL = NULL → UNKNOWN(NULL同士は等しくない) NULL <> NULL → UNKNOWN NULL IS NULL → TRUE ← 正しい書き方 NULL IS NOT NULL → FALSE WHERE句での UNKNOWN の影響 WHERE 条件が UNKNOWN になった行は結果セットから除外される WHERE col = NULL → 全行がUNKNOWN → 0行返る(意図どおりにならない) WHERE col IS NULL → NULLの行だけTRUE → 正しくNULLの行を取得
図1: NULL演算の結果一覧(AND・OR・NOT)

NULLとの比較は常にUNKNOWN

NULL = NULL    -- UNKNOWN(NULLはNULLと等しくない!)
NULL = 1       -- UNKNOWN
NULL <> 1      -- UNKNOWN
NULL > 1       -- UNKNOWN
NULL + 1       -- NULL(算術演算もNULL)
NULL || 'abc'  -- NULL(文字列結合もNULL)

AND・OR の3値論理表

ABA AND BA OR B
TRUEUNKNOWNUNKNOWNTRUE
FALSEUNKNOWNFALSEUNKNOWN
UNKNOWNUNKNOWNUNKNOWNUNKNOWN

WHEREの条件がUNKNOWNになった行はFILTERされる(結果に含まれない)ことを覚えておきましょう。


IS NULL / IS NOT NULL

NULLかどうかを判定するには IS NULL / IS NOT NULL を使います。= NULL は使えません。

-- 誤り: NULLを = で比較する(常にUNKNOWNになり0行返る)
SELECT * FROM customers WHERE phone = NULL;   -- 0行返る(意図どおりではない)

-- 正しい: IS NULLを使う
SELECT * FROM customers WHERE phone IS NULL;  -- NULLの行が返る

-- NOT NULLを確認
SELECT * FROM customers WHERE phone IS NOT NULL;

COALESCE() と NULLIF()

COALESCE()

COALESCE(値1, 値2, 値3, ...) は最初のNULLでない値を返します。デフォルト値を指定するときに便利です。

-- NULLの場合に '未登録' を表示
SELECT
    name,
    COALESCE(phone, '未登録') AS phone_display
FROM customers;

-- 複数のフォールバックを設定
SELECT COALESCE(nickname, first_name, '名無し') AS display_name
FROM users;

NULLIF()

NULLIF(値1, 値2) は2つの値が等しい場合にNULLを返します。ゼロ除算を防ぐのに役立ちます。

-- ゼロ除算を防ぐ
SELECT
    total_revenue,
    total_orders,
    total_revenue / NULLIF(total_orders, 0) AS avg_order_value
    -- total_orders が 0 の場合はNULLを返し、ゼロ除算エラーを回避
FROM sales_summary;

COUNT(*) vs COUNT(col)

NULLの扱いで結果が変わる代表例が COUNT です。

-- customersテーブル
-- id | name | email
-- 1  | 田中 | tanaka@example.com
-- 2  | 佐藤 | NULL              ← emailがNULL
-- 3  | 鈴木 | suzuki@example.com

SELECT
    COUNT(*)      AS all_count,    -- 3(全行をカウント、NULLも含む)
    COUNT(email)  AS email_count,  -- 2(NULLを除くemailがある行のみ)
    COUNT(DISTINCT email) AS unique_email  -- 2(NULLは除かれユニーク数)
FROM customers;

COUNT(*) は行数そのもの、COUNT(col) はその列がNULLでない行数です。意図に合わせて使い分けてください。

集約関数とNULL

SUM、AVG、MAX、MIN など他の集約関数もNULLを無視します。

-- scoresテーブル: 10, 20, NULL, 30
SELECT
    SUM(score),   -- 60(NULLは無視)
    AVG(score),   -- 20(60 / 3 ← NULLは分母にも含まれない)
    COUNT(*),     -- 4(NULLも含む行数)
    COUNT(score)  -- 3(NULLを除く)
FROM scores;

NOT IN にNULLが含まれる罠

NULLが引き起こすよくある罠 罠1: NOT IN にNULLが含まれる SELECT * FROM t WHERE id NOT IN (1, 2, NULL); → 0行返る(意図は id が 1,2 以外の行) 展開すると: id <> 1 AND id <> 2 AND id <> NULL ↑ UNKNOWN → 全行がFALSE ANDの一方がUNKNOWNになるため、結果は常にUNKNOWNかFALSE 修正方法 WHERE id NOT IN (SELECT id FROM t2 WHERE id IS NOT NULL) または NOT EXISTS を使う(NULLの罠がない) 罠2: COUNT(*) と COUNT(col) の違い customersテーブル 1 田中 email@xx 2 佐藤 NULL 3 鈴木 email@yy id=2のemailはNULL (未登録) 集計結果 COUNT(*) = 3 ↑ NULL含む全行 COUNT(email)= 2 ↑ NULL除く行数  SUM/AVGもNULL無視 罠3: AVG のNULL無視 scores: 10, 20, NULL, 30 AVG(score) = 20.0 ← (10+20+30) / 3 NULLは分子にも分母にも含まれない NULLを安全に扱う関数 COALESCE(col, 'デフォルト') → 最初のNULLでない値を返す NULLIF(col, 0) → col=0ならNULLを返す(ゼロ除算回避)
図2: NOT INにNULLが含まれると全件が除外される

NOT IN のリストにNULLが1つでも含まれると、結果が0行になるという罠があります。

-- manager_idテーブル
-- id: 1, 2, 3, 4(4はNULL)
SELECT *
FROM employees
WHERE id NOT IN (1, 2, NULL);
-- 結果: 0行!(意図は id が 1 でも 2 でもない行)

なぜか? NOT IN<> 1 AND <> 2 AND <> NULL に展開されます。id <> NULL は UNKNOWN になり、TRUE AND UNKNOWN は UNKNOWN になるため、すべての行がフィルタされてしまいます。

-- サブクエリにNULLが含まれるケース(よくある)
SELECT *
FROM employees
WHERE department_id NOT IN (
    SELECT department_id FROM departments WHERE active = false
    -- department_id に NULL が含まれると全件除外される
);

-- 解決策1: IS NOT NULLで除外
SELECT *
FROM employees
WHERE department_id NOT IN (
    SELECT department_id FROM departments
    WHERE active = false AND department_id IS NOT NULL
);

-- 解決策2: NOT EXISTSを使う(NULLの罠がない)
SELECT *
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM departments d
    WHERE d.department_id = e.department_id
    AND d.active = false
);

JOIN とNULL

LEFT JOIN でNULLが生成されます。NULLに対して条件を設定するときは注意が必要です。

-- 注文していない顧客(LEFT JOINの活用)
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.order_id IS NULL;  -- JOINで一致しなかった行はorder_idがNULL
-- 間違い: NULL は = NULL で比較できない
WHERE o.order_id = NULL  -- 常にUNKNOWN → 0行返る

まとめ

  • NULLは「値がない」状態で、0や空文字とは別物
  • NULLを含む比較は常にUNKNOWN(TRUEでもFALSEでもない)
  • NULLの判定には = NULL ではなく IS NULL / IS NOT NULL を使う
  • COALESCE() でデフォルト値を指定、NULLIF() でゼロ除算を防ぐ
  • COUNT(*) は全行、COUNT(col) はNULLを除いた行をカウント
  • NOT IN のリストにNULLが含まれると全件が除外される罠に注意

次回は トランザクション? 複数のDB操作をひとまとまりとして扱う仕組み。途中でエラーが起きたら全部元に戻す(ロールバック)ことで、データの整合性を保つ。 ACID特性? トランザクションが満たすべき4つの性質。Atomicity(原子性・全部成功か全部失敗)・Consistency(一貫性)・Isolation(分離性・他のトランザクションの影響を受けない)・Durability(永続性)。 特性について解説します。