요즘 쿼리 튜닝하다 보면 한 번쯤 이렇게 외치게 됩니다:
"아니 이거 왜 이렇게 느리지? 쿼리는 간단한데?"
그래서 오늘은 'NOT IN' 쿼리의 숨겨진 함정과 똑똑한 대안들을 정리해보려 해요. 실무에서 마주치는 흔한 상황을 예로 들고, 왜 문제가 생기는지, 어떻게 해결하면 좋을지 함께 살펴보죠.
SELECT p
FROM Post p
WHERE p.id NOT IN :postIds
이 쿼리, 얼핏 보면 아주 깔끔하고 단순하죠? 그런데 문제는 여기서부터 시작됩니다.
NOT IN
은 부정 조건이라 대부분의 DBMS가 인덱스를 제대로 활용하지 못해요. 결국 전체 테이블 스캔이나 풀 인덱스 스캔을 하게 되죠.
IN
에 많은 값을 넘기면 옵티마이저가 효율적인 실행 계획을 짜기 힘들고, 파싱과 최적화에 오버헤드가 생겨요. 특히 파라미터가 수천 개 넘어가면 지옥문이 열립니다
이건 정말 실수하기 쉬운 부분인데요:
SELECT * FROM Post WHERE id NOT IN (1, 2, NULL)
이 쿼리는 아무 결과도 안 나옵니다. 왜냐면 NULL
이 포함되면 비교 자체가 무효가 되거든요. 예상치 못한 버그의 원인이 되기 딱 좋죠.
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
는 행 단위로 조건을 평가하고, 매칭되는 첫 행을 찾자마자 탈출합니다. 덕분에 대량 데이터에서도 매우 효율적이고, NULL도 안전하게 회피할 수 있어요.
💡 대부분의 RDBMS에서
NOT EXISTS
가NOT IN
보다 성능이 더 좋고 실행 계획도 더 안정적이에요.
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
연산은 PK 인덱스를 적극 활용하므로, 성능이 안정적이죠.
NOT IN
은 정말 작고 정제된 데이터셋일 때만 사용하는 것이 좋습니다.NULL
이 포함될 가능성이 있는 컬럼에는 절대 쓰지 마세요.temp table
또는 WITH
절을 활용하는 것도 한 방법입니다.쿼리 하나가 시스템 전체 성능에 영향을 줄 수도 있어요. 특히 NOT IN
은 "이 정도면 괜찮겠지" 싶은 유혹을 주는 문법이라 더 위험해요.
실무에서 NOT IN 사용하고 있다면 꼭 점검해보시길 바랍니다!