대규모 데이터셋에서 NOT IN은 직관적인 만큼 위험합니다.
이번 글에서는 NOT IN의 성능 이슈와 이를 해결하는 대표적인 대안 두 가지를 정리하겠습니다.
다음과 같은 쿼리는 자주 쓰이지만, 성능 저하의 원인이 될 수 있습니다.
SELECT p
FROM Post p
WHERE p.id NOT IN (:postIds)
NOT IN의 문제점IN은 Range Scan이 가능한 반면,NOT IN은 대부분 Index Full Scan 또는 Table Full Scan으로 실행됩니다.WHERE id NOT IN (1, 2, NULL) -- 결과: 항상 빈 값 반환
NULL이 포함되면 모든 비교가 UNKNOWN 처리됨 → 필터링이 제대로 되지 않음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)NOT IN보다 훨씬 빠르고 예측 가능한 성능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
JOIN 시 인덱스를 사용할 수 있으며,NOT EXISTS가 대규모 필터링에 유리할까?NOT EXISTS는 내부적으로 Anti Join 전략 등으로 최적화됨NOT IN이 가진 NULL 비교 오류가 없음예: '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로도 가능하지만 가독성과 성능이 떨어질 수 있음NOT EXISTS 서브쿼리를 명시적으로 작성해야 함.not().exists() 또는 .notExists()로 표현 가능cb.not(cb.exists(...))로 작성| 방식 | 장점 | 단점 |
|---|---|---|
NOT IN | 직관적 | NULL 문제, 인덱스 미활용, 성능 저하 |
NOT EXISTS | 빠름, NULL 안전, 대용량에 강함 | 서브쿼리 작성 필요 |
LEFT JOIN + IS NULL | 인덱스 활용 가능, 쿼리 재활용 가능 | JOIN 비용 증가 가능 |
NOT IN을 쓸 땐 NULL 여부 반드시 확인!NOT EXISTS가 정답쿼리는 단순할수록 좋지만, 성능을 무시하면 장애의 씨앗이 됩니다.
익숙한 NOT IN 대신, NOT EXISTS를 기본 전략으로 삼는 습관이 성능에 큰 차이를 만들어냅니다.
✅ “무엇을 보여줄지”만큼 “무엇을 제외할지”도 중요합니다.
NOT EXISTS는 단순 문법이 아니라 성능과 안정성을 담보하는 전략입니다.