1. MySQL NOT IN 절이란 무엇인가? — 데이터 제외를 더 효율적으로 만들기
MySQL 데이터베이스와 작업할 때, 특정 값이나 조건을 “제외”하면서 데이터를 검색해야 하는 상황이 놀랍게도 많습니다. 예를 들어, 구독을 취소한 사용자들을 제외한 사용자 목록을 표시하거나, 블랙리스트에 나타나는 ID를 제외한 데이터를 집계하고 싶을 수 있습니다. 이러한 시나리오는 비즈니스와 개발 환경에서 자주 발생합니다. 여기서 NOT IN 절이 매우 유용해집니다.
NOT IN 절은 지정된 값이나 서브쿼리 결과와 일치하지 않는 데이터만 쉽게 추출할 수 있는 강력한 SQL 조건입니다. 목록을 사용한 간단한 제외 외에도 동적 서브쿼리와 결합하면 다양한 제외 패턴을 구현할 수 있습니다.
그러나 사용 방법에 따라 NOT IN에는 주의할 점과 잠재적 함정이 있습니다. 특히 NULL 값이 포함된 경우의 동작, 대형 데이터베이스에서의 성능 문제, 그리고 NOT EXISTS와의 차이점은 실무 수준에서 이해해야 할 중요한 포인트입니다.
이 기사에서는 MySQL NOT IN 절을 기본부터 고급 사용법까지 철저히 설명하며, 주의사항과 대안 제외 방법과의 비교를 구체적인 예시와 함께 다룹니다. SQL 초보자이든 이미 정기적으로 사용하든, 이 가이드는 귀중한 통찰을 제공합니다. 끝까지 읽어보시고 이 지식을 활용해 SQL 기술을 향상시키고 워크플로를 최적화하세요.
2. NOT IN의 기본 구문과 사용 예시
MySQL의 NOT IN 절은 여러 지정된 값 중 어느 것과도 일치하지 않는 레코드를 검색할 때 사용됩니다. 구문 자체는 간단하지만, 실제 시나리오에서 많은 상황에서 유용합니다. 여기서는 기본 구문과 실용적인 예시를 소개합니다.
[Basic Syntax]
SELECT column_name FROM table_name WHERE column_name NOT IN (value1, value2, ...);
간단한 목록을 사용한 제외
예를 들어, 이름이 “Yamada”나 “Sato”가 아닌 사용자를 검색하려면 다음 SQL 문을 작성합니다:
SELECT * FROM users WHERE name NOT IN ('Yamada', 'Sato');
이 쿼리를 실행하면 “Yamada”와 “Sato”라는 이름의 사용자 레코드를 제외한 모든 사용자 레코드를 검색합니다. 제외 목록은 쉼표로 구분된 값만 필요하므로 작성하고 이해하기 쉽습니다.
서브쿼리를 사용한 동적 제외
NOT IN 절은 고정 목록뿐만 아니라 괄호 안에 서브쿼리를 사용할 수도 있습니다. 이는 특정 조건을 만족하는 사용자 ID를 제외하고 싶을 때 특히 유용합니다.
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blacklist WHERE is_active = 1);
이 예시에서 blacklist 테이블에서 활성화된(is_active = 1) 사용자 ID가 제외되고, users 테이블에서 나머지 사용자가 검색됩니다. NOT IN을 서브쿼리와 결합하면 다양한 비즈니스 로직 요구사항에 유연하게 대응할 수 있습니다.
여러 조건 적용
여러 열에 걸친 제외 조건을 동시에 지정해야 한다면, NOT IN은 주로 단일 열 사용을 위해 설계되었습니다. 그러나 서브쿼리나 조인(JOIN)과 결합하면 더 복잡한 조건을 처리할 수 있습니다. 이는 나중에 고급 기법 섹션에서 자세히 설명하겠습니다.
보시다시피, NOT IN 절은 지정된 목록이나 서브쿼리 결과에 포함된 레코드를 제외한 모든 레코드를 검색할 때 매우 유용합니다. 추출하고 싶은 데이터를 시각화하며, 간단한 제외 목록과 서브쿼리를 효과적으로 연습해보세요.
3. NULL 값이 존재할 때의 중요한 주의사항
NOT IN 절을 사용할 때 흔히 간과되는 문제 중 하나는 NULL 값이 포함된 경우의 동작입니다. 이는 초보자뿐만 아니라 경험이 풍부한 SQL 사용자에게도 실수를 유발할 수 있는 고전적인 “함정”입니다.
그 이유는 NOT IN의 평가 논리가 일반 비교와 다르기 때문이며, NULL 값이 포함될 때 다르게 동작합니다.
NULL이 포함될 때의 동작
다음과 같은 테이블이 있다고 가정해 보겠습니다:
-- users table
id | name
---+------
1 | Sato
2 | Yamada
3 | Suzuki
4 | Tanaka
-- blacklist table
user_id
--------
1
NULL
이제 다음 SQL 문을 실행한다고 가정해 보겠습니다:
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM blacklist);
언뜻 보기에는 user_id = 1을 제외한 모든 사용자(즉, id = 2, 3, 4)가 반환될 것처럼 보일 수 있습니다. 그러나 실제로는 행이 전혀 반환되지 않습니다.
왜 행이 반환되지 않을까?
그 이유는 SQL의 3값 논리(TRUE / FALSE / UNKNOWN)에 있습니다.
NOT IN 목록에 NULL이 포함되면 비교 결과가 UNKNOWN이 되며, MySQL은 그 행들을 결과 집합에 포함하지 않습니다.
즉, 값이 목록의 어떤 항목과도 일치하지 않는다고 확정적으로 판단할 수 없기 때문에 전체 조건이 FALSE로 평가됩니다.
흔히 발생하는 문제 상황
이 문제는 서브쿼리를 사용할 때 자주 발생합니다. 블랙리스트나 구독 해제 목록에 NULL 값이 존재하면 데이터가 예상대로 조회되지 않을 수 있습니다.
“데이터가 반환되지 않는다”거나 “레코드가 제대로 제외되지 않는다”와 같은 문제는 종종 숨겨진 NULL 값 때문입니다.
대처 방안 및 해결 방법
NULL 값으로 인한 문제를 방지하려면 NOT IN 목록에서 NULL을 제외해야 합니다. 구체적으로는 서브쿼리 안에 IS NOT NULL 조건을 추가합니다.
SELECT * FROM users
WHERE id NOT IN (
SELECT user_id FROM blacklist WHERE user_id IS NOT NULL
);
이와 같이 수정하면 블랙리스트 테이블에 NULL 값이 있더라도, 해당 블랙리스트에 포함되지 않은 사용자를 올바르게 조회할 수 있습니다.
핵심 포인트
NOT IN목록에 NULL이 존재하면 쿼리가 행을 전혀 반환하지 않을 수 있습니다NOT IN을 사용할 때는 서브쿼리에 항상IS NOT NULL을 결합하세요- 데이터가 예상치 못하게 누락된 경우, 먼저 숨겨진 NULL 값을 확인하세요
4. NOT IN vs NOT EXISTS — 대안 비교
MySQL에서 제외 조건을 지정할 때 NOT EXISTS는 NOT IN의 또 다른 일반적인 대안입니다. 두 방법 모두 유사한 결과를 얻을 수 있지만 동작 방식, NULL 처리 및 성능 특성에서 차이가 있습니다. 이 섹션에서는 NOT IN과 NOT EXISTS를 비교하고 각각의 장단점을 설명합니다.
기본 구문 비교
[Exclusion Using NOT IN]
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blacklist WHERE user_id IS NOT NULL);
[Exclusion Using NOT EXISTS]
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b WHERE b.user_id = u.id
);
두 쿼리 모두 블랙리스트에 등록되지 않은 사용자를 조회합니다.
NULL 값 처리
NOT IN
- 목록이나 서브쿼리 결과에
NULL이 포함되면 쿼리가 예상대로 동작하지 않을 수 있습니다(행이 전혀 반환될 수 있음) - 이를 방지하려면 명시적인
IS NOT NULL조건이 필요합니다
NOT EXISTS
- 서브쿼리 결과에
NULL이 포함되어도 올바르게 동작합니다 - 일반적으로 NULL 값에 영향을 받지 않기 때문에 더 안전합니다
성능 차이
최적의 접근 방식은 데이터 양과 테이블 구조에 따라 다르지만, 일반적으로는 다음과 같습니다:
- 작은 데이터셋이나 고정된 목록의 경우
NOT IN이 충분히 성능을 발휘합니다 - 대규모 서브쿼리나 복잡한 조건에서는
NOT EXISTS또는LEFT JOIN이 더 나은 성능을 제공하는 경우가 많습니다
블랙리스트 레코드 수가 증가함에 따라 NOT EXISTS가 더 효율적이 되는 경우가 많습니다. MySQL 버전과 인덱싱에 따라 적절한 인덱스가 존재한다면 NOT EXISTS는 각 행에 대해 존재 여부를 검사하므로 매우 빠를 수 있습니다.
선택 가이드라인
- NULL 값이 존재할 가능성이 있다면 →
NOT EXISTS사용 - 고정된 목록이나 단순 값을 제외할 경우 →
NOT IN이면 충분 - 성능이 중요한 경우 → EXPLAIN으로 실행 계획을 확인하고 상황에 맞게 선택하세요(예: JOIN 또는
NOT EXISTS고려)
샘플 사례
NOT IN을 사용한 문제 예시
-- If blacklist.user_id contains NULL
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blacklist);
-- → May return zero rows
NOT EXISTS를 사용한 안전한 제외 예시
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b WHERE b.user_id = u.id
);
-- → Correct results regardless of NULL values
요약
NOT IN은 간단하지만 NULL 값에 취약합니다NOT EXISTS는 NULL에 강하고 프로덕션 환경에서 널리 사용됩니다- 데이터 특성과 요구 성능에 따라 선택하세요
5. 성능 고려 사항
대용량 데이터셋을 다룰 때 SQL 쿼리 성능은 매우 중요합니다. 조건과 데이터 양에 따라 NOT IN 또는 NOT EXISTS를 사용하면 실행 속도에 큰 차이가 발생할 수 있습니다. 이 섹션에서는 NOT IN 절의 성능 영향과 최적화 팁, 중요한 고려 사항을 중점적으로 살펴봅니다.
NOT IN의 성능 특성
NOT IN 절은 지정된 목록이나 서브쿼리 결과에 일치하지 않는 레코드를 검색합니다. 작은 목록이나 테이블에서는 효율적으로 동작하지만 다음 상황에서는 속도가 저하될 수 있습니다:
- 서브쿼리가 많은 행을 반환할 때
- 제외되는 컬럼에 인덱스가 없을 때
- 서브쿼리 결과에 NULL 값이 존재할 때
특히 서브쿼리에 수만 개 또는 수십만 개의 행이 포함되고 인덱스가 정의되지 않은 경우, MySQL은 전체 비교를 수행하게 되어 상당한 속도 저하가 발생할 수 있습니다.
인덱싱의 중요성
예를 들어 user_id와 같이 제외에 사용되는 컬럼에 인덱스를 추가하면 MySQL이 비교와 필터링을 보다 효율적으로 수행할 수 있습니다. 서브쿼리나 조인에 사용되는 컬럼은 가능한 경우 인덱스를 설정하는 것이 좋습니다.
CREATE INDEX idx_blacklist_user_id ON blacklist(user_id);
이와 같이 인덱스를 추가하면 NOT IN 및 NOT EXISTS 쿼리의 성능이 크게 향상됩니다. 
성능 비교: NOT IN vs NOT EXISTS
- 작은 고정 리스트:
NOT IN은 일반적으로 빠릅니다 - 큰 서브쿼리:
NOT EXISTS또는LEFT JOIN이 종종 더 효율적입니다
MySQL의 실행 계획(EXPLAIN 결과)은 버전 및 테이블 설계에 따라 달라지므로, 성능 최적화는 항상 실제 테스트를 통해 확인해야 합니다.
EXPLAIN으로 실행 계획 확인하기
어떤 쿼리가 더 나은 성능을 보이는지 판단하려면 MySQL의 EXPLAIN 명령을 사용합니다:
EXPLAIN SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM blacklist WHERE user_id IS NOT NULL);
이를 통해 어떤 인덱스가 사용되는지, 테이블이 전체 스캔되는지 등을 확인할 수 있으며, 이는 성능에 직접적인 영향을 미칩니다.
대규모 데이터셋에 대한 최적화 전략
- 서브쿼리 부하를 줄이기 위해 중간 결과를 임시 테이블에 저장
- 성능이 아직 부족하면 배치 처리나 캐싱 사용
LEFT JOIN ... IS NULL을 사용해 재작성 (일부 경우 속도 향상)
핵심 포인트
- 서브쿼리가 크거나 인덱스가 없을 때
NOT IN은 느려질 수 있습니다 - 적절한 인덱스 설계와 쿼리 검토로 성능을 크게 향상시킬 수 있습니다
NOT EXISTS또는LEFT JOIN을 고려하고, 항상 EXPLAIN으로 결과를 검증하세요
프로덕션 환경에서는 데이터 규모와 사용 빈도에 따라 가장 적합한 쿼리를 선택해야 합니다.
6. 일반적인 사용 사례 및 고급 기술
NOT IN 절은 단순한 제외에만 국한되지 않습니다. 고급 기법을 활용하면 보다 유연한 데이터 추출이 가능합니다. 여기서는 흔히 사용되는 패턴과 실용적인 기술을 소개합니다.
다중 컬럼 제외 (복합 키 제외)
NOT IN은 일반적으로 단일 컬럼에 사용되지만, 여러 컬럼의 조합을 제외해야 하는 경우도 있습니다. 이러한 상황에서는 NOT EXISTS 또는 LEFT JOIN이 더 적합합니다.
[예시: orders 테이블에서 customer_id와 product_id의 특정 조합 제외]
SELECT * FROM orders o
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b
WHERE b.customer_id = o.customer_id
AND b.product_id = o.product_id
);
이것은 블랙리스트에 등록된 모든 “customer_id × product_id” 조합을 제외합니다.
부분 일치 제외 (NOT LIKE 사용)
NOT IN은 정확히 일치하는 경우에만 작동하므로, 특정 문자열 패턴을 제외할 때는 NOT LIKE를 사용하십시오. 예를 들어, 이메일 주소가 “test@”로 시작하는 사용자를 제외하려면:
SELECT * FROM users WHERE email NOT LIKE 'test@%';
여러 패턴을 한 번에 제외하려면 조건을 AND로 결합하십시오:
SELECT * FROM users
WHERE email NOT LIKE 'test@%'
AND email NOT LIKE 'sample@%';
대규모 제외 목록 처리
수백 또는 수천 개의 값을 NOT IN 안에 직접 나열하면 가독성이 떨어지고 성능에 영향을 줄 수 있습니다.
이런 경우에는 전용 테이블이나 서브쿼리를 사용하여 제외 목록을 보다 깔끔하게 관리하십시오:
-- Example: Store exclusion list in blacklist table
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blacklist WHERE user_id IS NOT NULL);
집계 함수와 결합
집계 조건을 포함하는 서브쿼리와 함께 NOT IN을 사용할 수도 있습니다.
[예시: 이번 달에 주문을 하지 않은 고객 조회]
SELECT * FROM customers
WHERE id NOT IN (
SELECT customer_id FROM orders
WHERE order_date >= '2025-06-01'
AND order_date < '2025-07-01'
);
서브쿼리 대신 JOIN 사용
경우에 따라 LEFT JOIN과 IS NULL을 결합하여 동일한 결과를 얻을 수 있습니다.
성능과 가독성을 기준으로 가장 적절한 방법을 선택하십시오.
SELECT u.*
FROM users u
LEFT JOIN blacklist b ON u.id = b.user_id
WHERE b.user_id IS NULL;
이 방법은 서브쿼리 성능이 불확실하거나 인덱스가 효과적인 경우에 특히 유용합니다.
핵심 포인트
- 다중 컬럼 제외에는
NOT EXISTS또는 JOIN 사용 - 부분 문자열 제외에는
NOT LIKE와 결합 - 대규모 제외 목록은 테이블이나 서브쿼리로 관리
JOIN + IS NULL도 성능을 향상시킬 수 있음
7. FAQ (자주 묻는 질문)
다음은 MySQL NOT IN 절에 대한 자주 묻는 질문과 일반적인 어려움에 대한 답변입니다. 답변은 실제 상황에서 자주 검색되는 실용적인 문제에 초점을 맞춥니다.
Q1. NOT IN과 IN의 차이점은 무엇인가요?
A.
IN은 지정된 목록 중 어느 하나와 일치하는 데이터를 검색하고, NOT IN은 목록의 어느 값과도 일치하지 않는 데이터만 검색합니다. 구문은 거의 동일하지만, 특정 값을 제외하려면 NOT IN을 사용해야 합니다.
Q2. NOT IN을 사용할 때 NULL 값이 존재하면 어떻게 되나요?
A.
목록이나 서브쿼리에 NULL 값이 포함되면 NOT IN은 행을 반환하지 않거나 예상치 못한 결과를 초래할 수 있습니다. IS NOT NULL을 사용하여 NULL을 명시적으로 제외하는 것이 가장 안전합니다.
Q3. NOT IN과 NOT EXISTS 중 어느 것을 선택해야 할까요?
A.
- NULL 값이 존재할 가능성이 있거나 서브쿼리가 포함된 경우
NOT EXISTS가 더 신뢰할 수 있습니다. - 고정된 목록이나 간단한 제외 경우
NOT IN이 충분히 작동합니다. - 실행 계획과 데이터 양에 따라 성능이 달라질 수 있으므로, 구체적인 상황에 맞게 선택하십시오.
Q4. NOT IN을 사용하는 쿼리가 느릴 때는 어떻게 해야 하나요?
A.
- 제외 조건에 사용되는 컬럼에 인덱스를 추가하십시오.
- 서브쿼리 결과의 크기를 줄이거나 데이터를 임시 테이블에 정리하십시오.
NOT EXISTS또는LEFT JOIN ... IS NULL을 사용하도록 쿼리를 재작성하는 것을 고려하십시오.- EXPLAIN을 사용해 실행 계획을 분석하고 병목 현상을 파악하십시오.
Q5. 여러 컬럼을 기준으로 어떻게 제외할 수 있나요?
A.
NOT IN은 단일 컬럼 사용을 위해 설계되었으므로, 여러 컬럼에 걸친 복합 제외가 필요할 때는 NOT EXISTS 또는 LEFT JOIN을 사용하세요. 서브쿼리 안에서 여러 컬럼 조건을 결합합니다.
Q6. 서브쿼리가 많은 행을 반환할 때 주의해야 할 점은 무엇인가요?
A.
서브쿼리가 많은 행을 반환하면 NOT IN은 성능 저하가 발생할 수 있습니다. 인덱스 사용, 임시 테이블 활용, 혹은 쿼리를 재구성하여 서브쿼리를 가능한 작게 유지하세요.
Q7. 기대한 결과가 나오지 않을 때 확인해야 할 사항은 무엇인가요?
A.
- NULL 값이 의도치 않게 포함되지 않았는지 확인하세요
- 서브쿼리를 별도로 실행하여 결과를 확인하세요
- WHERE 절이나 JOIN 로직에 오류가 없는지 점검하세요
- 필요하다면 MySQL 버전별 동작 및 공식 문서를 검토하세요
8. 결론
MySQL NOT IN 절은 특정 조건을 만족하지 않는 데이터를 효율적으로 조회하기 위한 매우 유용한 구문입니다. 단순 제외 리스트부터 서브쿼리를 활용한 유연한 필터링까지, 다양한 실무 시나리오에 적용할 수 있습니다.
하지만 실제 사용에서는 NULL 값 처리와 대용량 데이터셋에서의 성능 저하와 같은 중요한 고려사항이 있습니다. NULL 값으로 인한 예상치 못한 결과가 없는 쿼리나 대형 서브쿼리로 인한 실행 속도 저하와 같은 문제는 초보자와 숙련 개발자 모두가 주의해야 합니다.
NOT EXISTS와 LEFT JOIN ... IS NULL 같은 대안도 이해하면 보다 안전하고 효율적인 SQL 쿼리를 작성할 수 있습니다. 목표와 데이터 규모에 맞는 최적의 방법을 항상 선택하세요.
핵심 정리
NOT IN은 단순 제외 조건에 효과적입니다- 항상 NULL 값을 방지하세요 (
IS NOT NULL을 습관화) - 성능이 우려될 경우 인덱스 전략을 고려하거나
NOT EXISTS및 JOIN 대안을 사용하세요 - 실행 계획(EXPLAIN)을 통해 효과를 항상 검증하세요
SQL “함정”을 피하고, 이 글에서 다룬 개념을 일상 업무와 학습에 적용하여 스마트한 데이터 추출을 실천하세요.


