본문으로 건너뛰기
개발 머꼬
개발 노트SQL
hohyeon.dev17

NOT IN 서브쿼리가 조용히 0건을 돌려준 이유

  • #Debugging
  • #Engineering Note
  • #SQL

문제 발생

차단하지 않은 사용자의 글만 뽑는 쿼리가 갑자기 빈 결과를 돌려줬습니다.

SELECT * FROM dev_posts
WHERE author_id NOT IN (SELECT user_id FROM blocked_users);

오류도 없고 경고도 없었습니다. blocked_usersuser_idNULL인 행 하나가 들어간 날부터였습니다.

원인 분석

3값 논리 때문입니다. MySQL 문서가 규칙을 그대로 적습니다 — SQL 표준을 따르기 위해 IN()은 왼쪽 표현식이 NULL일 때뿐 아니라, 목록에서 매칭을 찾지 못했고 목록의 표현식 중 하나가 NULL일 때도 NULL을 반환합니다.

NOT IN은 문서 설명대로 NOT (expr IN (...))과 같으므로 그 결과도 그대로 이어집니다.

5 IN (1, 2, NULL)      → NULL   (FALSE가 아니다)
5 NOT IN (1, 2, NULL)  → NOT NULL → NULL

WHERE는 참인 행만 남기므로 NULL은 탈락합니다. 결국 차단 목록에 NULL이 한 개라도 있으면 모든 행이 사라집니다. 오류가 아니라 표준에 맞는 동작이라 조용합니다.

IN(긍정형)에서는 이 문제가 덜 눈에 띕니다. 매칭되는 행은 여전히 참이고, 매칭 안 되는 행만 FALSE 대신 NULL이 되어 어차피 탈락하기 때문입니다.

해결 방안

  1. NOT EXISTS로 바꿉니다. NULL에 영향을 받지 않고, 대개 실행 계획도 좋습니다.
SELECT p.* FROM dev_posts p
WHERE NOT EXISTS (
  SELECT 1 FROM blocked_users b WHERE b.user_id = p.author_id
);
  1. 또는 서브쿼리에서 NULL을 걸러냅니다.
WHERE author_id NOT IN (SELECT user_id FROM blocked_users WHERE user_id IS NOT NULL)
  1. 더 근본적으로는 스키마에서 막습니다. 그 컬럼이 NULL이면 안 되는 값이라면 NOT NULL 제약을 겁니다. 이 버그가 애초에 생기지 않습니다.

  2. LEFT JOIN ... IS NULL 패턴도 안전합니다. 팀이 익숙한 형태를 고르되, NOT IN만은 서브쿼리 결과에 NULL 가능성이 있을 때 쓰지 않습니다.

  3. 왼쪽 컬럼도 확인합니다. author_id가 nullable이면(MEOKKO에서는 탈퇴 사용자 보존을 위해 실제로 그렇습니다) 그 행도 조건 결과가 NULL이 되어 조용히 빠집니다. 의도한 동작인지 매번 확인합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.