NOT IN은 오른쪽의 모든 값과 다르다고 확인되어야 TRUE가 됩니다. 같은 값은 없지만 오른쪽 결과에 NULL이 있으면, 그 NULL과도 다른지 확정할 수 없어 UNKNOWN이 됩니다. WHERE는 UNKNOWN을 통과시키지 않으므로 목록에 없는 값까지 빠질 수 있습니다.

이 글은 PostgreSQL 18의 스칼라 비교를 기준으로 합니다. 아래 표와 변형 문제는 직접 만든 연습이며, NULL을 삭제하거나 NOT EXISTS로 바꾸는 것이 모든 업무에서 정답이라는 뜻은 아닙니다.

FALSE와 UNKNOWN을 같은 답으로 외우지 않습니다

‘회원 8은 차단 목록에 없다’는 판단을 하고 싶다고 가정해 보세요. 차단 목록이 3과 5라면 두 값 모두 다르다고 확인할 수 있습니다. 그런데 두 번째 값이 NULL이라면 그 값이 실제로 8인지 아닌지 알 수 없습니다. WHERE 결과에서는 FALSE와 UNKNOWN이 모두 제외되지만, 두 판단이 같은 것은 아닙니다. 후속 조건이나 부정 연산을 붙일 때 차이가 드러납니다.

직접 계산하는 스칼라 NOT IN 판정표
왼쪽 값오른쪽 결과NOT IN 결과WHERE 통과
33, NULLFALSE아니요
83, NULLUNKNOWN아니요
83, 5TRUE예
8빈 집합TRUE예
NULL3, 5UNKNOWN아니요
NULL빈 집합TRUE예

첫 행은 이미 같은 값 3이 있으므로 거짓입니다. 두 번째 행은 3과 다르지만 NULL과 다른지 모릅니다. 빈 집합에는 같음을 보여 주는 값도 비교가 불확실한 값도 없으므로 스칼라 NOT IN이 참입니다. ‘왼쪽이 NULL이면 언제나 UNKNOWN’이라고 외우면 마지막 행을 놓칩니다. 이 예외는 실제 하위 쿼리가 한 행도 반환하지 않는 경우에 해당합니다.

회원 세 명과 차단 ID 두 개로 결과를 만듭니다

교육용 입력과 원본 NOT IN 쿼리
WITH member(id) AS (
  VALUES (3), (8), (12)
), blocked(member_id) AS (
  VALUES (3), (NULL::integer)
)
SELECT id
FROM member
WHERE id NOT IN (SELECT member_id FROM blocked)
ORDER BY id;

결과는 0행입니다. 회원 3은 차단 목록의 3과 같아서 제외됩니다. 회원 8과 12는 NULL 때문에 조건을 참으로 확정할 수 없어 제외됩니다. 여기서 ‘차단되었으니 모두 제외된다’고 설명하면 원인을 잘못 짚은 것입니다. 8과 12가 차단되었다는 사실을 얻은 것이 아니라, 현재 표현식이 TRUE를 만들지 못한 것입니다.

변형 A: NULL 차단 ID를 비교 대상에서 제외한다는 규칙을 명시합니다.
SELECT id
FROM member
WHERE id NOT IN (
  SELECT member_id FROM blocked
  WHERE member_id IS NOT NULL
)
ORDER BY id;

위 WITH 입력에 변형 A를 적용하면 8과 12가 남습니다. 그러나 이는 단순한 문법 수리가 아니라, NULL인 차단 기록을 제외해도 된다는 판단을 반영한 것입니다. NULL이 데이터 수집 오류를 뜻한다면 먼저 그 오류를 확인해야 할 수 있습니다. 결과를 원하는 모양으로 만들기 위해 불명확한 기록을 무조건 지우지는 마세요.

NOT EXISTS는 일치하는 행의 존재를 묻습니다

변형 B: 현재 회원과 같은 차단 ID가 한 행이라도 있는지 검사합니다.
SELECT m.id
FROM member AS m
WHERE NOT EXISTS (
  SELECT 1 FROM blocked AS b
  WHERE b.member_id = m.id
)
ORDER BY m.id;

여기서는 각 회원에 대해 상관 조건이 TRUE인 차단 행이 존재하는지 확인합니다. NULL 차단 ID와의 등호 비교는 TRUE가 아니어서 일치 행이 되지 않습니다. 따라서 현재 입력에서는 A와 B 모두 8과 12를 반환합니다. 두 쿼리의 결과가 이번 예시에서 같다는 사실만으로, 모든 데이터에서 서로 바꿔 써도 된다고 결론 내리면 안 됩니다.

회원 쪽에 NULL ID가 있고 차단 목록이 비어 있지 않다면 차이가 생길 수 있습니다. NULL을 거른 NOT IN도 왼쪽 NULL과 남은 값의 비교를 확정하지 못합니다. 반면 등호 조건의 NOT EXISTS는 NULL과 같은 일치 행을 찾지 못해 그 회원 행을 남길 수 있습니다. 실제 테이블에서 회원 ID가 NOT NULL인지, NULL 회원을 포함해야 하는지 먼저 확인하세요.

빈 집합·같은 값·NULL을 바꿔 세 번 풉니다

  1. 하위 쿼리의 결과 열만 따로 적고, NULL과 중복값 유무를 표시합니다.
  2. 왼쪽 값과 정확히 같은 값이 있는지 먼저 확인합니다.
  3. 같은 값이 없다면 비교를 확정할 수 없는 NULL이 남는지 확인합니다.
  4. 하위 쿼리 결과를 빈 집합으로 바꿔 예상 결과가 어떻게 달라지는지 적습니다.
  5. NOT EXISTS로 바꿀 때는 상관 조건과 왼쪽 열의 NULL 허용 여부를 함께 설명합니다.

오답 노트에는 정답 SQL 한 줄보다 ‘목록에 없음’과 ‘다름을 확인할 수 있음’을 혼동했다는 이유를 적어 보세요. 복습할 때 숫자만 바꾸지 말고 NULL을 왼쪽에 넣거나, 하위 쿼리를 비우거나, 같은 값을 두 개 넣어 보세요. 중복값은 판정 논리 자체를 바꾸지 않지만, NULL과 빈 집합은 결론을 바꿀 수 있습니다.

정답을 가리고 확인해 보세요

이 글을 위해 만든 학습 예시이며 공식 기출문제나 판매 카드의 인용이 아닙니다.

회원 ID가 9이고 차단 ID 하위 쿼리 결과가 2와 NULL입니다. 원본 NOT IN의 판정은 무엇인가요? 차단 ID가 NULL인 행을 하위 쿼리에서 제외하면 어떻게 바뀌나요?

정답과 해설 확인원본은 UNKNOWN이므로 WHERE를 통과하지 못합니다. NULL인 행을 제외하면 9는 2와 다르므로 TRUE가 됩니다.

9와 NULL이 다른지 확정할 수 없다는 점이 원본의 핵심입니다. NULL을 제외한 변형은 그 기록을 비교 대상에서 빼도 된다는 별도 업무 판단이 있어야 합니다.