TL

3値論理とNULLの意味論・落とし穴

NULL のせいで件数が合わない、IN が空を返す、UNIQUE が重複を通す。直感に反する結果を3値論理の原理から一掃し、確実に意図どおりのクエリを書けるようになります。

応用データベースSQLNULL3値論理クエリ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.SQL の比較は TRUE/FALSE/UNKNOWN の3値論理で、NULL を含む比較はすべて UNKNOWN になり WHERE は UNKNOWN の行を返さない。
  • 2.集約関数は NULL を無視するが COUNT(*) は数える。NOT IN にサブクエリの NULL が混じると結果が全部消えるなど、否定で罠が集中する。
  • 3.UNIQUE 制約とほとんどの DB は NULL を互いに非等価として扱い重複を許す。GROUP BY・DISTINCT・ORDER BY では逆に NULL 同士を同一視する。

NULL は「値」ではなく「不明」というマーカー

SQL の NULL は 0 でも空文字でもなく、「値が存在しない/不明(unknown)」を表すマーカーです。この一点から、SQL の論理体系は真偽2値ではなく TRUE / FALSE / UNKNOWN の3値論理(3VL) になります。x = NULL の真偽は「不明な何かが特定の値に等しいか」という問いなので、答えは TRUE でも FALSE でもなく UNKNOWN です。

ここを取り違えると、「件数が合わない」「NOT IN が空を返す」「UNIQUE なのに重複が入る」といった直感に反する結果がすべて謎のまま残ります。本稿は標準 SQL の3値論理を基準に、比較・論理演算・集約・結合・一意制約での振る舞いを原理から整理します(SQL の基礎は SQL の基礎 を参照)。

比較演算:NULL が絡むと結果は UNKNOWN

NULL に対する = <> < > などの比較は、両辺のどちらかが NULL なら結果は必ず UNKNOWN です。等しいかどうかすら判定できないからです。

NULL =  1     -> UNKNOWN
NULL <> 1     -> UNKNOWN
NULL =  NULL  -> UNKNOWN   -- 「不明 と 不明」が等しいかも不明
NULL <  1     -> UNKNOWN

WHERE 句は 条件が TRUE の行だけを返します。UNKNOWN の行は返しません(FALSE と同じ扱いで脱落)。だから NULL の行を狙うには専用述語 IS NULL / IS NOT NULL を使います。これらは3値論理の例外で、必ず TRUE か FALSE を返します。

NULL = NULL は真にならない

WHERE col = NULLWHERE col <> 'A' は、col が NULL の行を決して拾いません。前者は常に UNKNOWN、後者も col が NULL なら UNKNOWN だからです。NULL を含む列を「A 以外」で絞ったつもりが NULL 行が漏れる、というのは最頻出のバグです。col <> 'A' OR col IS NULL のように明示するか、col IS DISTINCT FROM 'A'(NULL を別物として扱い TRUE/FALSE のみ返す述語)を使います。

論理演算:UNKNOWN を含む AND / OR / NOT

3値論理の AND・OR・NOT は、UNKNOWN を「TRUE かもしれないし FALSE かもしれない」と読むと一貫して理解できます。確定できる場合だけ TRUE/FALSE を返し、左右の値で結論が変わるなら UNKNOWN を返します。

演算規則の要点代表例
AND片方が FALSE なら結果 FALSE(短絡)。それ以外で UNKNOWN が絡めば UNKNOWN`FALSE AND UNKNOWN` は FALSE
OR片方が TRUE なら結果 TRUE(短絡)。それ以外で UNKNOWN が絡めば UNKNOWN`TRUE OR UNKNOWN` は TRUE
NOTTRUE↔FALSE を反転、UNKNOWN は UNKNOWN のまま`NOT UNKNOWN` は UNKNOWN

ポイントは NOT UNKNOWN = UNKNOWN です。x <> 5 が UNKNOWN なら、その否定 NOT (x <> 5) も UNKNOWN のまま反転しません。「条件か、その否定か、どちらかは必ず行を拾う」という2値論理の常識(排中律)が3値論理では崩れるため、WHERE pWHERE NOT p の和が全行にならないのです。

-- score に NULL がある場合、この2本を合わせても全行は得られない
SELECT * FROM t WHERE score > 50;       -- NULL 行は UNKNOWN で脱落
SELECT * FROM t WHERE NOT (score > 50); -- NULL 行はここでも UNKNOWN で脱落

CHECK 制約だけは UNKNOWN を「通す」

同じ UNKNOWN でも、評価する場所で扱いが反転します。WHEREHAVING、結合の ONUNKNOWN を不採用(行を返さない) とします。一方 CHECK 制約は UNKNOWN を許容(違反としない) します。つまり CHECK (age >= 0) の列に NULL を入れても、NULL >= 0 は UNKNOWN なので制約は通ってしまいます。NULL を弾きたいなら別途 NOT NULL が要ります。

集約とグループ化:無視する場面、同一視する場面

集約関数(SUM AVG MAX MIN COUNT(col))は 入力の NULL を捨ててから計算します。ここに2つの落とし穴があります。

  • AVG(col) は NULL 行を分母に入れません。「欠損を 0 とみなした平均」とは値が変わります。0 扱いにしたいなら AVG(COALESCE(col, 0)) のように明示変換が必要です。
  • COUNT(col) は非 NULL の個数、COUNT(*) は行数です。両者は NULL があると一致しません。
全行 NULL の SUM は 0 ではなく NULL

対象がすべて NULL(または0行)のとき、SUMMAX0 ではなく NULL を返します。COUNT だけは常に 0 以上の整数を返します。集計結果を後段で計算に使うときは、COALESCE(SUM(col), 0) のように NULL を吸収しないと、NULL が伝播して最終結果まで NULL に化けます。

集約とは逆に、GROUP BYDISTINCTORDER BY は NULL 同士を「同じ」とみなします。比較では NULL = NULL が UNKNOWN なのに、グループ化では複数の NULL が1つのグループにまとまります。SQL は文脈ごとに「NULL の等価性」を別ルールで定義している、と捉えるのが正確です。ORDER BY での NULL の並び位置(先頭か末尾か)は実装依存で、PostgreSQL は NULLS FIRST / NULLS LAST で制御できます。

結合:外部結合が生む NULL と NOT IN の罠

結合条件 ON a.x = b.y でも、xy が NULL の行は UNKNOWN になりマッチしません。さらに 外部結合(OUTER JOIN)は、相手が見つからない側の列を結果に NULL として埋めます(結合全般は JOIN(結合))。この「結合が作り出す NULL」は元データには存在しないため、後続の WHERE で意図せず行を落とす原因になります。LEFT JOIN の直後に右表の列を WHERE で素の比較にかけると、結合で埋まった NULL が UNKNOWN になり、実質 INNER JOIN に化けるのが典型です。

最大の罠が NOT IN です。

-- t2.id に1つでも NULL があると、この結果は常に空になる
SELECT * FROM t1
WHERE t1.id NOT IN (SELECT id FROM t2);

x NOT IN (a, b, NULL)x <> a AND x <> b AND x <> NULL に展開され、最後の x <> NULL が UNKNOWN です。AND は片方が FALSE でない限り UNKNOWN を消せないので、x がリストのどの値とも違っても結果は TRUE になれず UNKNOWN。WHERE は UNKNOWN を返さないため、全行が脱落します。

NOT IN ではなく NOT EXISTS / 反結合を使う

サブクエリ側に NULL が混じり得るなら NOT IN を避け、NOT EXISTSLEFT JOIN ... WHERE 右表.key IS NULL(反結合)に置き換えます。NOT EXISTS は行の存在だけを2値で判定し UNKNOWN を生まないため、NULL があっても直感どおりに動きます。IN(肯定)側は NULL があっても TRUE の行は拾えるため NOT IN ほど壊れませんが、安全側に倒すなら EXISTS 系が無難です。

一意制約と主キー:UNIQUE は NULL を重複と見なさない

UNIQUE 制約は「重複を許さない」制約ですが、その判定にも3値論理が効きます。標準 SQL と多くの DB(PostgreSQL・Oracle・MySQL など)は、2つの NULL を等しくないものとして扱うため、UNIQUE 列に NULL を複数行入れても重複違反になりません。NULL = NULL が TRUE でない以上、「同じ値が2つある」とは判定できないからです。

CREATE TABLE u (code TEXT UNIQUE);
INSERT INTO u VALUES (NULL);  -- OK
INSERT INTO u VALUES (NULL);  -- これも OK(重複扱いされない)
INSERT INTO u VALUES ('A');
INSERT INTO u VALUES ('A');  -- ここで初めて重複違反
文脈NULL 同士の扱い結果
= による比較 / WHERE / JOIN ON等価でない(UNKNOWN)マッチしない・行が脱落
UNIQUE 制約(既定)等価でないNULL は何行でも入れられる
GROUP BY / DISTINCT等価(同一視)NULL は1グループ・1行に集約
IS NOT DISTINCT FROM等価(NULL=NULL を真とする)NULL を含めて TRUE/FALSE で判定

実務的な含意は明確です。一意であってほしい業務キーを UNIQUE だけで守ると、NULL が複数すり抜けて「実質的な重複」が生まれます。主キーは定義上 NOT NULL なので安全ですが、それ以外の一意キーは UNIQUE + NOT NULL をセットで付けるのが原則です。NULL を許す一意列で「NULL 同士も1つに制限したい」場合は、PostgreSQL 15 以降の UNIQUE NULLS NOT DISTINCT のような明示オプションが必要になります。これはスキーマ設計(正規化)でキーの NOT NULL 性を詰める段階で判断すべき点です。

まとめ

  • SQL の比較・論理は TRUE / FALSE / UNKNOWN の3値論理で、NULL が絡む比較はすべて UNKNOWN。WHEREONHAVING は UNKNOWN の行を返さない。
  • NOT UNKNOWN は UNKNOWN のままで排中律が崩れるため、WHERE pWHERE NOT p を合わせても全行にならない。NULL 狙いは IS NULL を使う。
  • 集約は NULL を無視し(AVG の分母・全 NULL の SUM が NULL になる点に注意)、GROUP BY / DISTINCT / ORDER BY は逆に NULL 同士を同一視する。
  • NOT IN はサブクエリの NULL で結果が全消えするため NOT EXISTS / 反結合に置換する。外部結合が生む NULL は後続 WHERE で行を落とす。
  • UNIQUE は NULL を重複と見なさないため、業務上の一意キーは UNIQUE + NOT NULL で守る。

NULL を一貫して扱う鍵は、「NULL は値ではなくマーカーであり、文脈ごとに等価性のルールが変わる」と理解することです。比較では非等価、グループ化では等価、という二面性さえ押さえれば、上の罠はすべて同じ原理から説明できます。クエリの挙動をさらに掘るなら クエリ最適化 も参照してください。

データベース Article

3値論理とNULLの意味論・落とし穴を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

データベース

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 5

導入後に効く点

集約関数は NULL を無視するが COUNT(*) は数える。NOT IN にサブクエリの NULL が混じると結果が全部消えるなど、否定で罠が集中する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
5

判断チェックリスト

  • 自社の用途が「データベース / SQL」に近いか確認する。
  • 強みである「SQL の比較は TRUE/FALSE/UNKNOWN の3値論理で、NULL を含む比較はすべて UNKNOWN になり WHERE は UNKNOWN の行を返さない。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

データベースSQLNULL3値論理クエリデータベースSQLNULL
参考: 公式情報