NULL과 삼중 논리
NULL의 세 값 논리와 정렬 규칙을 이해하고 IS NULL과 명시적 순서로 누락 없는 조회를 만듭니다.
게시되지 않은 게시글의 published_at은 빈 문자열도 0도 아닙니다.
아직 값이 없거나 알 수 없다는 NULL입니다.
이를 = NULL로 비교하면 오류가 나지 않으면서도 한 행도 찾지 못하기 때문에 초보자가 가장 오래 놓치는 조회 오류가 됩니다.
회원 게시판에서는 초안 게시글의 게시 시각과 선택 메모가 NULL일 수 있습니다.
NULL을 값 하나로 보지 않고 비교 결과가 알 수 없음이 되는 세 값 논리로 읽으면 NOT IN, 집계, 정렬에서 생기는 예외도 같은 원리로 설명할 수 있습니다.
NULL 비교의 오류
아직 게시되지 않은 게시글을 찾으려고 published_at = NULL을 사용합니다.
구문은 실행되지만 어떤 값과 NULL의 동등 비교도 TRUE가 되지 않습니다.
NULL끼리 비교해도 결과는 알 수 없음입니다.
SELECT id, title, published_at
FROM query_post_demo
WHERE published_at = NULL;초안·검토 중 게시글이 두 개 있어도 결과는 비어 있습니다. 문법 오류가 없어서 더 위험한 의미 오류입니다.
오류 실행 결과Empty set
-- 기대: published_at이 없는 DRAFT 게시글x = NULL은 TRUE나 FALSE가 아니라 알 수 없음입니다.
WHERE는 TRUE인 행만 통과시키므로 알 수 없음 행은 제외됩니다.
x <> NULL도 알 수 없음이어서 반대 행을 찾지 못합니다.
따라서 존재 여부는 IS NULL과 IS NOT NULL이라는 전용 조건식으로 검사해야 합니다.
TRUE·FALSE·UNKNOWN
NULL이 산술식이나 대부분의 비교에 참여하면 결과도 NULL 또는 알 수 없음이 됩니다.
TRUE AND UNKNOWN은 알 수 없음, FALSE AND UNKNOWN은 FALSE, TRUE OR UNKNOWN은 TRUE입니다.
이 규칙 때문에 NULL 허용 컬럼을 포함한 NOT IN 서브쿼리는 목록에 NULL 하나만 있어도 모든 비교가 알 수 없음이 되어 빈 결과를 만들 수 있습니다.
값이 없음과 빈 문자열, 0, ‘미정’ 상태 코드는 서로 다른 업무 의미이므로 저장 단계에서 구분해야 합니다.
IS NULL, IS NOT NULL, COALESCE는 표준 SQL입니다.
MySQL은 NULL-안전한 동등성 연산자 <=>를 제공하지만 제품 전용 문법이므로 일반 조회에서는 IS NULL과 명확한 분기를 우선합니다.
MySQL 오름차순은 NULL을 먼저, 내림차순은 나중에 두는 경향이 있지만 NULL 위치를 업무 규칙으로 삼으려면 표현식으로 명시해야 합니다.
NULL 존재 검사
미공개 게시글은 게시 시각의 부재를 직접 검사하고, 상태 규칙도 함께 확인합니다. 두 컬럼이 서로 모순되는 행이 없는지도 진단할 수 있습니다.
SELECT id, title, status, published_at
FROM query_post_demo
WHERE published_at IS NULL
AND status = 'DRAFT'
ORDER BY created_at, id;NULL인 행이 정상적으로 통과하고, 보관된 게시글을 미게시 목록에 포함할지 상태 조건으로 결정합니다.
개선 실행 결과id | title | status | published_at
13 | JOIN 계획 | DRAFT | NULL
14 | GROUP BY 검토 | DRAFT | NULLCOALESCE(published_at, created_at)처럼 NULL을 표시용 값으로 바꾸는 것은 SELECT 결과에서 유용하지만, 원본 의미를 바꾸지는 않습니다.
WHERE에서 무조건 대체값을 넣으면 ‘실제 게시 시각’과 ‘대신 보여 준 시각’이 섞일 수 있습니다.
저장 무결성은 이후 검사 또는 트랜잭션 규칙으로 별도 보장합니다.
NULL 정렬 순서
미공개 게시글을 뒤로 보내고, 공개 게시글은 최근 게시 순서로 정렬하고 싶다면 NULL 여부 표현식을 첫 기준으로 둡니다. MySQL에서 불리언 식은 0과 1로 정렬됩니다.
SELECT id, title, published_at
FROM query_post_demo
ORDER BY
published_at IS NULL ASC,
published_at DESC,
id DESC;게시 시각이 있는 행은 (published_at IS NULL) = 0, 없는 행은 1이어서 게시된 행이 먼저 옵니다.
다른 DBMS로 이식할 때는 NULLS FIRST/LAST 지원 여부나 CASE 표현식을 확인합니다.
중요한 것은 기본 동작을 외우기보다 원하는 위치를 쿼리에 적는 것입니다.
NULL 허용 컬럼이 많아질수록 ‘값이 아직 없음’, ‘적용 대상이 아님’, ‘수집 오류’를 NULL 하나로 뭉개기 쉽습니다. 분석에서 이 셋을 구분해야 한다면 상태 컬럼이나 별도 이벤트가 필요합니다. 반대로 모든 선택값에 ‘NONE’, 빈 문자열 같은 가짜 값을 넣으면 타입과 제약의 의미가 약해집니다. NULL 가능성은 데이터 모델의 의사결정입니다.
세 값 논리를 익히려면 진리표를 실제 SELECT로 확인하는 것이 좋습니다.
SELECT 1 AND NULL, SELECT 0 AND NULL, SELECT 1 OR NULL, SELECT NOT NULL을 실행하고 TRUE·FALSE·NULL 결과를 기록하세요.
이어서 NULL 허용 값을 가진 작은 임시 테이블에서 IN, NOT IN, EXISTS, NOT EXISTS를 비교하면 다음 장의 서브쿼리 오류를 미리 이해할 수 있습니다.
집계 함수도 차이가 있습니다.
COUNT(*)는 행을 세고 COUNT(published_at)은 NULL이 아닌 값만 세므로 둘의 차이는 미게시된 행 수가 될 수 있습니다.
NULL을 0으로 바꾼 뒤 평균을 내면 ‘측정되지 않음’을 실제 0회으로 해석하게 되므로 집계 전에 업무 의미를 확인합니다.
API 직렬화에서도 NULL 정책을 명확히 해야 합니다.
필드를 생략하는 것과 publishedAt: null을 보내는 것은 부분 업데이트에서 다른 의미가 될 수 있습니다.
부분 수정 요청에서 필드 생략은 유지, 명시적 NULL은 값 제거로 해석한다면 데이터베이스 UPDATE도 두 경우를 구분해야 합니다.
유일성 제약 역시 NULL 허용 컬럼에서는 여러 NULL을 허용할 수 있어 ‘값이 있을 때만 유일’이라는 규칙이 됩니다.
이 동작이 의도인지 확인하지 않고 UNIQUE 하나로 필수 식별자를 보장했다고 생각하면 안 됩니다.
값의 부재가 허용되지 않는 후보 키에는 NOT NULL을 함께 두고, 상태와 시각의 조합 규칙은 검사나 애플리케이션 트랜잭션으로 검증합니다.
삭제 시각, 게시 시각처럼 상태와 함께 움직이는 컬럼은 짝 규칙을 갖습니다.
공개 상태인데 published_at이 NULL이거나 초안 상태인데 값이 있으면 조회마다 예외 분기가 생깁니다.
MySQL 8.4의 검사로 단일 행 안의 조합을 검사할 수 있지만, 기존 데이터가 규칙을 만족하는지 먼저 진단해야 합니다.
외부 시스템에서 부분 데이터가 늦게 도착하는 경우에는 일시적 불일치를 허용할지 수신 테이블을 분리할지도 결정합니다.
NULL 허용 여부는 편의 문법이 아니라 상태 전이와 수집 신뢰도를 반영한 모델입니다.
인덱스에도 NULL 행이 포함되므로 IS NULL 조회가 무조건 전체 스캔이라고 단정할 수 없습니다.
값 분포와 인덱스 선두 컬럼, 반환 비율에 따라 옵티마이저 선택이 달라집니다.
NULL을 자주 찾는 업무라면 실제 데이터 건수로 실행 계획을 확인하고, 단지 성능을 위해 의미 없는 기본값을 저장하지 않습니다.
백업·CSV 내보내기에서 NULL과 빈 문자열이 같은 표기로 손실되지 않는지도 점검합니다. 데이터 사전에는 각 NULL 허용 컬럼의 부재 의미와 생성 시점을 한 문장으로 기록합니다. 가져오기 도구의 NULL 표기도 함께 고정합니다.
회원 게시판의 NULL 정책은 이후 집계·JOIN·서브쿼리에서 같은 의미로 유지되어야 합니다.
published_at IS NULL을 미게시로 쓰면서 상태 컬럼은 PUBLISHED인 행을 허용하면 어느 쿼리를 기준으로 삼느냐에 따라 통계가 달라집니다.
먼저 상태 전이 규칙을 하나로 정하고, 시각은 그 규칙의 증거가 되게 합니다.
5장의 LEFT JOIN에서는 짝이 없어 생긴 오른쪽 NULL과 실제 NULL 허용 값이 한 결과에 함께 나타나므로 자식 PK로 둘을 구분합니다.
6장의 NOT IN은 목록에 NULL 하나가 섞이면 전체 조건식이 알 수 없음이 될 수 있어 NOT EXISTS로 관계 부재를 표현합니다.
데이터 사전에는 허용 이유, 생성 시점, 해소 조건을 함께 기록하고 가져오기 단계에서도 빈 문자열을 자동으로 NULL로 바꿀지 명시합니다.
부재 의미를 한 장에서만 설명하지 않고 모든 소비 쿼리에 같은 규칙으로 연결해야 합니다.
NULL 허용 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 의미 | NULL이 미정·부재·해당 없음 중 무엇을 뜻하는가? |
| 조건식 | IS NULL/IS NOT NULL로 존재를 검사했는가? |
| 논리 | 알 수 없음이 AND·OR·NOT에 미치는 영향을 확인했는가? |
| 표시 | COALESCE로 바꾼 값이 원본 의미와 구분되는가? |
| 정렬 | NULL 위치를 결과 규칙에 명시했는가? |
테스트 데이터에는 반드시 NULL과 비NULL 행을 함께 넣습니다.
NOT을 붙였을 때 두 집합이 전체를 정확히 나누는지 보지 말고 알 수 없음 집합이 따로 남는지 확인해야 합니다.
연습 문제
모든 게시글에서 게시 시각이 없는 초안·검토 글을 게시 시각이 있는 글보다 뒤에 정렬하세요.
게시 시각이 없는 행끼리는 최신 작성 시각과 큰 id 순서로 보여야 합니다.
published_at = NULL이나 빈 문자열 비교는 사용하지 마세요.
해설과 예시 답안
전체 목록에서 NULL 여부를 첫 정렬 키로 두면 게시된 글과 미게시 글을 함께 유지하면서 위치를 정할 수 있습니다. 상태를 DRAFT로 필터하면 모두 NULL이어서 첫 정렬 키가 의미 없다는 점도 구분합니다.
SELECT id, title, status, published_at, created_at
FROM query_post_demo
ORDER BY
published_at IS NULL ASC,
published_at DESC,
created_at DESC,
id DESC;게시 시각이 있는 글이 먼저, NULL인 초안·검토 글이 뒤에 오는지 확인합니다.
정말 미게시 글만 필요하다면 WHERE published_at IS NULL을 추가하고 첫 정렬식은 제거할 수 있습니다.
핵심 정리
- NULL은 일반 값이 아니며 비교 결과를 알 수 없음으로 만듭니다.
- 존재 여부는 = NULL이 아니라 IS NULL과 IS NOT NULL로 검사합니다.
- COALESCE는 표시 대체와 원본 저장 의미를 구분해 사용합니다.
- NULL 정렬 위치는 DBMS 기본값에 맡기지 않고 결과 규칙으로 명시합니다.
다음 장에서는 선택한 값을 계산·문자열·날짜 함수로 가공하고 집계 단위로 축약합니다.