비어 있는 값이 남기는 문제
테이블을 만들고, 회원 데이터를 넣었다. 전화번호를 입력하지 않은 사람을 찾으려고 이렇게 썼다.
SELECT name
FROM users
WHERE phone = NULL;
결과는 비어 있다. 분명 전화번호를 입력하지 않은 행이 있는데도.
오타도 아니고, 테이블 이름도 맞다. 그런데 작동하지 않는다. 이 문장이 왜 틀렸는지를 이해하려면, NULL이 무엇인지를 다시 생각해야 한다.
NULL은 비어 있음이 아니다
처음 NULL을 보면 빈칸처럼 느껴진다. 아무것도 없는 상태. 0이나 빈 문자열과 비슷한 것이라고.
하지만 데이터베이스에서 세 가지는 완전히 다르다.
0은 숫자 영(零)이다. 명확하게 정해진 값이다. ''는 빈 문자열이다. 내용이 없는 문자열이지만, 그 자체로 하나의 값이다. NULL은 다르다. 값이 없는 게 아니라, 아직 알 수 없거나 기록되지 않은 상태를 나타낸다.

회원가입 폼에서 전화번호 칸을 비워두었을 때, 어떤 시스템은 그 값을 빈 문자열('')로 저장하고, 어떤 시스템은 NULL로 저장한다. 중요한 것은 둘이 같은 뜻이 아니라는 점이다. NULL로 저장된 값은 "전화번호가 없다"기보다, "전화번호를 아직 알 수 없거나 기록하지 않았다"는 상태에 가깝다.
이 차이가 작아 보이지만, 쿼리를 짤 때 생각보다 많은 곳에서 문제가 된다.
등호로 비교할 수 없는 이유
앞의 쿼리가 작동하지 않는 이유는 여기에 있다.
WHERE phone = NULL
이 조건은 "phone과 NULL이 같은가?"를 묻는다. 그런데 NULL은 알 수 없는 값이다. 알 수 없는 값이 다른 알 수 없는 값과 같은지는, 역시 알 수 없다.
그래서 NULL = NULL의 결과는 true가 아니다. NULL이다.
데이터베이스는 이 비교 자체를 "알 수 없음"으로 처리한다. WHERE 절에서 결과가 NULL이면 그 행은 결과에 포함되지 않는다. 전화번호가 없는 행도 전부 걸러진다.
NULL을 찾으려면 등호 대신 IS NULL을 써야 한다.
SELECT name
FROM users
WHERE phone IS NULL;
반대로 값이 있는 행만 보고 싶다면 IS NOT NULL을 쓴다.
SELECT name
FROM users
WHERE phone IS NOT NULL;
IS NULL은 등호가 아니다. "이 칸이 알 수 없는 상태인가?"를 묻는 별도의 표현이다.
알 수 없음은 전파된다
NULL의 특성 중 가장 조심해야 할 것이 있다. 연산에 NULL이 끼면, 결과도 NULL이 된다.
SELECT 100 + NULL; -- NULL
SELECT NULL * 5; -- NULL
이유는 하나다. 알 수 없는 값이 포함된 계산의 결과 역시, 알 수 없다.
상품 테이블에서 가격과 수량을 곱해 총가격을 구하는 쿼리가 있다고 하자.
SELECT item_id,
price * quantity AS total
FROM items;
| item_id | price | quantity | total |
|---|---|---|---|
| 1 | 100 | 2 | 200 |
| 2 | NULL | 3 | NULL |
| 3 | 200 | NULL | NULL |
| 4 | NULL | NULL | NULL |

가격이나 수량 중 하나라도 NULL이면 총가격도 NULL이 된다. 계산이 잘못된 게 아니다. 모르는 값이 들어간 결과는 모를 수밖에 없다는 뜻이다.
이 동작이 문제가 될 때는 COALESCE로 처리한다. COALESCE(값, 대체값)은 값이 NULL일 때 대체값을 돌려준다.
SELECT item_id,
COALESCE(price, 0) * COALESCE(quantity, 0) AS total
FROM items;
다만, NULL을 무조건 0으로 채우는 것이 항상 옳지는 않다. "모른다"는 상태를 0으로 바꾸는 순간, 데이터의 의미가 달라질 수 있다. 어떤 값으로 대체할지는 데이터가 무엇을 뜻하는지에 따라 판단해야 한다.
집계에서는 다르게 다뤄진다
일반 연산에서 NULL이 전파되는 것과 달리, SUM, AVG, MAX, MIN 같은 집계함수는 보통 NULL을 건너뛰고 값이 있는 행을 기준으로 계산한다.
-- scores 열의 값: 80, NULL, 90, NULL, 70
SELECT SUM(score), -- 240 (NULL 제외)
AVG(score), -- 80 (240 ÷ 3, NULL 제외)
MAX(score), -- 90
MIN(score) -- 70
FROM scores;
SUM은 NULL을 0으로 처리하지 않는다. 그냥 없는 것처럼 건너뛴다. AVG도 전체 행 수로 나누지 않고, 값이 있는 행 수만으로 나눈다. 값이 5개인데 NULL이 2개라면 3으로 나눈다.
집계함수 중 COUNT는 두 가지 방식으로 동작한다.
SELECT COUNT(*), -- 5 (전체 행 수, NULL 포함)
COUNT(score) -- 3 (값이 있는 행만)
FROM scores;

COUNT(*)는 행 자체를 센다. NULL이 있어도 행이 존재하면 포함된다. COUNT(score)는 score 열에 값이 있는 행만 센다. NULL이 있는 행은 제외된다.
이 차이를 모르면 전체 회원 수와 전화번호가 있는 회원 수를 같은 것으로 착각하기 쉽다. 둘은 다른 질문이다.
비어 있는 값이 남기는 문제
NULL을 다루는 것은 문법의 문제이기 전에, 데이터를 어떻게 읽을지의 문제다.
테이블에 NULL이 있다는 것은 그 순간 데이터를 알 수 없었다는 기록이다. 가입할 때 전화번호를 입력하지 않았거나, 아직 배송지가 정해지지 않았거나, 측정이 누락되었거나. NULL은 그 상황을 그대로 보존한다.
WHERE phone = NULL이 작동하지 않는 것도, NULL + 1이 NULL이 되는 것도, COUNT(*)와 COUNT(col)이 다른 것도, 전부 같은 이유다. 알 수 없는 값은 알 수 없는 채로 남는다.
그 알 수 없음을 0으로 채울지, 제외할지, 그대로 둘지는 쿼리를 짜는 사람이 결정해야 한다. 데이터베이스는 모른다고 솔직하게 말한다. 그 솔직함을 어떻게 읽을지는 질문하는 사람의 몫이다.
