"값이 없다"가 아니라 **"모른다"**를 뜻하는 표식. SQL이 참·거짓에 더해 3값 논리를 쓰는 이유다.
비교하면 참도 거짓도 아니다
SELECT NULL = NULL; -- NULL (true 가 아니다)
SELECT NULL <> 1; -- NULL
SELECT NULL > 0; -- NULL
-- 그래서 비교 연산자로는 절대 찾을 수 없다
SELECT * FROM member WHERE phone = NULL; -- ❌ 항상 0건
SELECT * FROM member WHERE phone IS NULL; -- ✅ 전용 연산자를 쓴다
"모르는 값"과 "모르는 값"이 같은지도 모른다 — 이 한 줄이 아래 모든 기이함의 원인이다.
WHERE는 참인 행만 통과시킨다
-- status 가 'X' 가 아닌 행을 모두 원했는데
SELECT * FROM orders WHERE status <> 'X';
-- status 가 NULL 인 행은 빠진다 (NULL <> 'X' 는 NULL 이라 통과 못 한다)
-- 의도대로 하려면
SELECT * FROM orders WHERE status IS DISTINCT FROM 'X'; -- PostgreSQL
SELECT * FROM orders WHERE NOT (status <=> 'X'); -- MySQL (NULL-safe 비교)
집계 함수는 NULL을 건너뛴다
-- 값: 10, 20, NULL
SELECT count(*), count(amount), sum(amount), avg(amount) FROM t;
-- 3 2 30 15
-- ^ NULL 제외 ^ 제외 ^ 30/2 이지 30/3 이 아니다
avg가 예상과 다른 값을 내는 대표적 원인이다. NULL을 0으로 치려면 명시해야 한다.
SELECT avg(COALESCE(amount, 0)) FROM t; -- 10 (30/3)
UNIQUE 제약은 NULL을 여러 개 허용한다
CREATE TABLE t (email VARCHAR(255) UNIQUE);
INSERT INTO t VALUES (NULL), (NULL), (NULL); -- 전부 성공한다
NULL끼리는 같은지 모르므로 중복 판정이 불가능하다. "이메일은 선택 입력이지만 넣으면 유일해야 한다"가 자연스럽게 표현되는 셈이다.
-- PostgreSQL 15+ 는 NULL 도 중복으로 볼지 고를 수 있다
CREATE TABLE t (email VARCHAR(255) UNIQUE NULLS NOT DISTINCT); -- NULL 은 하나만
정렬에서의 위치도 엔진마다 다르다
-- PostgreSQL: ASC 면 NULL 이 마지막, DESC 면 처음 (기본)
SELECT * FROM t ORDER BY amount ASC NULLS FIRST; -- 명시할 수 있다
-- MySQL: ASC 면 NULL 이 처음 (반대다!) · NULLS FIRST 문법이 없어 우회한다
SELECT * FROM t ORDER BY amount IS NULL, amount ASC;
같은 쿼리가 두 엔진에서 다른 순서를 낸다 — 이식할 때 조용히 깨지는 지점이다.
실무 원칙
- ① 가능하면 NOT NULL + DEFAULT 로 애초에 NULL 을 안 만든다
- ② "값이 없음" 과 "0" · "빈 문자열" 을 구분해야 할 때만 NULL 을 쓴다
- ③ 조인 키에는 절대 NULL 을 두지 않는다 (조인이 조용히 행을 버린다)
면접 함정
- ❌ "NULL은 빈 값이다" → 빈 문자열
''과 NULL은 다르다. Oracle만 예외적으로 둘을 같게 다룬다. - ❌ "count(*)와 count(칼럼)은 같다" → 후자는 NULL을 세지 않는다.