NOT IN은 왜 위험할까?

김상진 ·2025년 3월 29일

Problem Solving

목록 보기
10/10

대규모 데이터셋에서 NOT IN은 직관적인 만큼 위험합니다.
이번 글에서는 NOT IN의 성능 이슈와 이를 해결하는 대표적인 대안 두 가지를 정리하겠습니다.


❗️문제 상황

다음과 같은 쿼리는 자주 쓰이지만, 성능 저하의 원인이 될 수 있습니다.

SELECT p 
FROM Post p
WHERE p.id NOT IN (:postIds)

🚨 NOT IN의 문제점

1. 인덱스를 잘 활용하지 못함

  • IN은 Range Scan이 가능한 반면,
  • NOT IN은 대부분 Index Full Scan 또는 Table Full Scan으로 실행됩니다.

2. NULL 포함 시 항상 false

WHERE id NOT IN (1, 2, NULL) -- 결과: 항상 빈 값 반환
  • NULL이 포함되면 모든 비교가 UNKNOWN 처리됨 → 필터링이 제대로 되지 않음

3. 서브쿼리 또는 바인딩 값이 클 경우 실행 계획 최적화 어려움

  • 바인딩 값이 많을수록 파싱 오버헤드 증가
  • 실행 계획 캐시 적중률도 낮아짐

✅ 실전에서 사용하는 최적화 방법

1. 🔄 NOT EXISTS로 대체 (가장 안정적)

SELECT p 
FROM Post p
WHERE NOT EXISTS (
  SELECT 1 
  FROM Post temp 
  WHERE temp.id = p.id 
    AND temp.id IN (:postIds)
)
  • NOT EXISTS행 단위 평가로, 첫 매칭이 확인되면 바로 평가 종료 (Early Exit)
  • 대부분의 DBMS에서 NOT IN보다 훨씬 빠르고 예측 가능한 성능

2. 🔗 LEFT JOIN + IS NULL 패턴

SELECT p 
FROM Post p 
LEFT JOIN (
  SELECT temp.id 
  FROM Post temp 
  WHERE temp.id IN (:postIds)
) filtered ON p.id = filtered.id
WHERE filtered.id IS NULL
  • 조인 후 null 값을 조건으로 걸러내는 방식
  • JOIN 시 인덱스를 사용할 수 있으며,
  • 서브쿼리 결과가 작을 때 매우 효율적

🔍 왜 NOT EXISTS가 대규모 필터링에 유리할까?

  • 조기 종료(Early Exit): 조건에 해당하는 값이 하나라도 존재하면 바로 평가 종료
  • 인덱스 활용 가능: 서브쿼리 조건이 인덱스를 타면 빠른 확인 가능
  • 옵티마이저 최적화: NOT EXISTS는 내부적으로 Anti Join 전략 등으로 최적화됨
  • NULL 안전성 보장: NOT IN이 가진 NULL 비교 오류가 없음

🧱 실전 예시: 다대다(N\:N) 관계 필터링

예: 'Java' 또는 'Spring' 태그가 붙지 않은 게시글 조회

SELECT p.*
FROM Post p
WHERE NOT EXISTS (
  SELECT 1
  FROM Post_Tag pt
  WHERE pt.post_id = p.id
    AND pt.tag IN ('Java', 'Spring')
)
  • 다대다 관계에서 특정 조건 제외할 때 매우 직관적이고 빠름
  • LEFT JOIN + IS NULL로도 가능하지만 가독성과 성능이 떨어질 수 있음

☕️ JPA/QueryDSL에서는?

  • JPQL: NOT EXISTS 서브쿼리를 명시적으로 작성해야 함
  • QueryDSL: .not().exists() 또는 .notExists()로 표현 가능
  • JPA Criteria: cb.not(cb.exists(...))로 작성

📌 요약 정리

방식장점단점
NOT IN직관적NULL 문제, 인덱스 미활용, 성능 저하
NOT EXISTS빠름, NULL 안전, 대용량에 강함서브쿼리 작성 필요
LEFT JOIN + IS NULL인덱스 활용 가능, 쿼리 재활용 가능JOIN 비용 증가 가능

🧪 팁

  • NOT IN을 쓸 땐 NULL 여부 반드시 확인!
  • 데이터가 많다면 EXPLAIN으로 실행 계획 꼭 확인
  • 다대다 관계, 부정 조건(포함되지 않은 것 찾기)에서는 NOT EXISTS가 정답
  • 프론트에서 다중 필터 선택 기능이 있을 경우, 미선택 조건을 배제하는 데 유용

🙋‍♂️ 마무리

쿼리는 단순할수록 좋지만, 성능을 무시하면 장애의 씨앗이 됩니다.
익숙한 NOT IN 대신, NOT EXISTS를 기본 전략으로 삼는 습관이 성능에 큰 차이를 만들어냅니다.

✅ “무엇을 보여줄지”만큼 “무엇을 제외할지”도 중요합니다.
NOT EXISTS는 단순 문법이 아니라 성능과 안정성을 담보하는 전략입니다.

profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글