TT Lab
はじめる
学ぶ 学習パス コース

SQL実戦

照会とフィルタ — 三値論理と条件の順序

TT Labで続きを見る

一言でいうと

SQLのWHEREは、真の行だけを通過させ、NULLが混ざると結果が真でも偽でもない不明になるので、条件を書くときは、常に3つ目の値を念頭に置く必要があります。

なぜ必要なのか

ほとんどのプログラミング言語は、真と偽の2つの値だけを扱います。SQLは3つの値を扱います。真、偽、そして不明です。NULLが関与するすべての比較は不明になり、WHEREは真の行だけを通過させるので、不明は偽のように扱われます。

この1つの違いが、実務で静かなバグを生みます。条件を反転させたのに、行数の合計が全体と合わなくなる現象が代表的です。

SELECT count(*) FROM customers WHERE city = '서울';       -- 48
SELECT count(*) FROM customers WHERE city <> '서울';      -- 335
-- 합이 383 인데 전체는 400 이다. 나머지 17 은 city 가 NULL 인 행이다.

<>でも引っかからないので、NULLの行は、どちら側にもありません。このとき必要なのがIS DISTINCT FROMです。この演算子は、NULLを1つの値のように比較してくれるので、上の例では352を返します。

どう動くのか

よく使う条件の性格を整理すると、次のとおりです。

条件 意味 注意点
BETWEEN a AND b a以上b以下 両端の値を含みます。日付範囲に使うと、最終日の午前0時より後が抜けることがあります
IN (...) リストのうちの1つ リストにNULLがあり、NOT INを使うと、結果がまるごと消えます
LIKE 'kim%' 前方一致 インデックスで探索可能
LIKE '%kim%' 部分一致 開始点がわからないので、通常のインデックスでは探索不可
IS NULL 値がない = NULLは常に0行
COALESCE(a, b) aがNULLならb 表示用の置換に使い、条件に使うとインデックスを使えません

CASEは、上から最初に真になる枝を選びます。そのため、境界が重なる条件を並べるとき、順序がそのまま優先順位になります。金額の区間を分けるとき、大きい値から書くのが慣例である理由です。

ページネーションも押さえておく必要があります。ORDER BYなしでLIMITだけを使うと、どの行が出るかは保証されません。そして、並べ替えの基準に同点があると、ページの間で行が重複したり抜けたりします。並べ替えは、常に一意な値まで含めてtie-breakを作る必要があります。実務では、ORDER BY created_at DESC, id DESCのように、最後に主キーを付けます。

現場での姿

OFFSET方式のページネーションは、後ろのページに行くほど遅くなります。OFFSET 10000は、1万行を読んで捨てる作業だからです。代替案は、最後に見た値を基準に続きを読むカーソル方式です。条件がWHERE (created_at, id) < (마지막값, 마지막id)の形になり、インデックス探索で開始地点をすぐに見つけられます(プレースホルダーは、最後の値と最後のidです)。

NULLが作る3値論理

SQLの比較は、真・偽ではなく、真・偽・不明です。これが条件句の動作を変えます。

NULL = NULL      → UNKNOWN (참이 아니다)
NULL <> 1        → UNKNOWN
NULL IS NULL     → TRUE      ← 이것만 참이 된다

WHEREは真の行だけを残します。UNKNOWNは偽のように捨てられます。そのため、次のようなことが起こります。

-- status 가 NULL 인 행은 두 질의 어디에도 안 나온다
select * from orders where status = 'done';
select * from orders where status <> 'done';

全体を扱うには、NULLを明示する必要があります。

where status is distinct from 'done'    -- NULL 도 "다르다" 로 본다
where status <> 'done' or status is null
where coalesce(status, '') <> 'done'

IS DISTINCT FROMが最もすっきりしています。NULLを1つの値のように比較します。

集計でも違います。count(*)は行を数えますが、count(col)はNULLでないものだけを数えます。avg・sumもNULLを無視するので、欠損を0として扱いたいなら、coalesceで埋める必要があります。

条件の順序は性能を変えない(たいてい)

WHERE a = 1 AND b = 2で順序を入れ替えても、オプティマイザーが自動的に決めます。人が気にすべきは、順序ではなく、インデックスを使える形かどうかです。

-- ❌ 인덱스를 못 쓴다 — 컬럼에 함수를 씌웠다
where date(created_at) = '2026-09-06'
where upper(email) = 'A@B.COM'

-- ✅ 범위로 바꾸거나 표현식 인덱스를 만든다
where created_at >= '2026-09-06' and created_at < '2026-09-07'
create index on users ((upper(email)));

LIKEも同様です。'abc%'はインデックスを使いますが、'%abc'は使えません。後ろから探す必要があるなら、trigramインデックスか全文検索を使います。

実行計画を読む最小限

explain (analyze, buffers) select …;

analyzeは実際に動かしてみて、buffersはどれだけ読んだかを見せてくれます。見るべきものは3つです。

次のラボですること

実際のEコマースのスキーマで、BETWEEN、IN、LIKE、COALESCE、CASE、IS DISTINCT FROMを順に使ってみます。特に、NULLが混ざったカラムで条件を反転させたときに、行数が合わない経験を自分ですることになります。