본문 바로가기

database

NOT IN 쿼리는 신중하게 사용하자

728x90

개발을 하다 보면 특정 조건에 해당하지 않는 데이터를 조회해야 할 때가 많습니다. 이럴 때 흔히 사용하는 게 바로 NOT IN이죠. 예를 들어 다음과 같은 형태입니다:

SELECT * FROM users WHERE status NOT IN ('inactive', 'banned');

혹은 MongoDB에서는 이렇게도 사용하죠:

 
db.users.find({ status: { $nin: ["inactive", "banned"] } });

딱 봐도 직관적이고 쓰기 편해 보입니다. 하지만 실무에서는 조심해서 써야 할 몇 가지 함정이 존재합니다. 이번 글에서는 제가 겪은 실제 사례를 바탕으로 NOT IN 쿼리의 문제점과 이를 최적화하는 방법까지 정리해보려고 합니다.

⚠️ NOT IN이 느려지는 이유

  1. 인덱스를 타지 못하는 경우가 많다
  2. IN은 인덱스를 잘 타지만, NOT IN은 대부분의 경우 인덱스를 무시합니다. 조건 자체가 "해당하지 않는 것"을 찾는 것이기 때문에, DB는 전체 데이터를 스캔할 수밖에 없게 되죠.
  3. NULL 값 포함 여부에 따라 예외가 발생할 수 있다 (특히 SQL)
SELECT * FROM users WHERE id NOT IN (1, 2, NULL);

 

NULL은 비교 불가이기 때문에, SQL에서는 NOT IN이 제대로 작동하지 않는 예외가 생깁니다.

 

 

NOT IN의 리스트에 NULL이 포함되어 있으면, 결과가 아예 안 나오는 경우도 있습니다. 예를 들어 다음 쿼리는 아무 것도 반환하지 않을 수 있습니다:

  1. 복잡한 서브쿼리와 함께 쓰일 경우 성능 저하
     
    blocked_users가 커지면 커질수록 이 서브쿼리를 계산하고 NOT IN을 적용하는 데 시간이 오래 걸립니다. 그리고 서브쿼리에 NULL이 포함되면 다시 위와 같은 문제로 이어질 수도 있습니다.
SELECT * FROM users WHERE id NOT IN ( SELECT user_id FROM blocked_users WHERE reason = 'spam' );
SELECT * FROM users WHERE id NOT IN (1, 2, NULL);

그럼 어떻게 최적화할 수 있을까?

1. NOT EXISTS 또는 LEFT JOIN ... IS NULL 로 변경 (SQL)

성능 이슈가 있는 경우에는 NOT EXISTS나 LEFT JOIN 방식으로 바꿔보는 걸 추천합니다:

NOT EXISTS SELECT * FROM users u WHERE NOT EXISTS (
	SELECT 1 
    FROM blocked_users b 
    WHERE b.user_id = u.id AND b.reason = 'spam' 
 ); 
 
 LEFT JOIN 방식
 
 SELECT u.* 
 FROM users u LEFT JOIN blocked_users b ON b.user_id = u.id AND b.reason = 'spam' 
 WHERE b.user_id IS NULL;

이 방식은 보통 옵티마이저가 더 효율적인 실행 계획을 만들 수 있도록 도와줍니다.

2. MongoDB에서는 $nin을 $expr이나 $lookup으로 대체 고려

MongoDB에서도 $nin 쿼리가 인덱스를 타지 않아서 느린 경우가 많습니다. 이럴 때는 $lookup으로 조인해서 존재 여부로 필터링하는 방법을 사용할 수 있습니다:

db.users.aggregate([
  {
    $lookup: {
      from: "blocked_users",
      localField: "_id",
      foreignField: "user_id",
      as: "blocked"
    }
  },
  {
    $match: { blocked: { $size: 0 } }
  }
])

단, lookup은 오히려 비싸질 수도 있으니 쿼리 조건/데이터 크기에 따라 트레이드오프를 잘 따져야 합니다.

3. 사전 필터링 + 캐싱

차라리 아예 NOT IN으로 쓸 값들을 메모리나 Redis에 캐싱해두고, 어플리케이션 레벨에서 필터링하거나 조건을 바꿔서 IN 조건으로 재구성하는 방식도 고려할 수 있습니다.

 

마치며

NOT IN은 편하긴 하지만, 잘못 쓰면 인덱스도 못 타고 예외도 발생하며, 성능도 크게 저하될 수 있는 잠재적인 성능 폭탄입니다. 실제로 저희 팀에서도 MongoDB $nin이 들어간 쿼리 때문에 병목이 생겨서, $lookup으로 바꾸고 나서야 응답 시간이 절반 이하로 줄었던 경험이 있습니다.

꼭 필요한 경우가 아니라면, 대체 쿼리 방식이나 데이터 구조 개선을 고민해보는 걸 추천드립니다. 혹시 여러분도 유사한 이슈를 겪으셨다면, 댓글로 공유해 주세요. 👇

728x90