공공부하자개발 · 영어 학습 노트
SQL
SQL 중급서브쿼리·조인·함수·DDL·인덱스0/11 완료
  • 01서브쿼리: 스칼라·인라인 뷰·상관 서브쿼리, DB별 차이
  • 02EXISTS·IN·NOT IN 과 NULL 함정
  • 03조인 심화: INNER·OUTER·SELF·CROSS, Oracle (+)
  • 04집합 연산: UNION·UNION ALL·INTERSECT·MINUS/EXCEPT
  • 05조건 로직: CASE·DECODE·IIF
  • 06NULL 처리 함수
  • 07문자·날짜 함수 DB별 비교
  • 08DDL 과 제약 조건
  • 09인덱스 기초
  • 10집계와 GROUP BY·HAVING
  • 11뷰·시퀀스·자동 증가
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › SQL 중급 › 02 / 11

EXISTS·IN·NOT IN 과 NULL 함정

세미 조인·안티 조인, 3값 논리
섹션 6진행 0 / 11
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6정리‹ 이전다음 ›

2. 핵심 원리

2.1 EXISTS 와 세미 조인

EXISTS (서브쿼리) 는 서브쿼리가 결과를 한 행이라도 반환하면 참입니다. 값이 아니라 존재 여부만 보므로, 서브쿼리의 SELECT 목록에 무엇을 쓰든 결과에 영향이 없어 관례상 SELECT 1 을 씁니다.

바깥 쿼리 한 행과 서브쿼리가 조건으로 연결되는 방식이어서, EXISTS 는 개념적으로 "세미 조인(semi join)"입니다. 일반 JOIN 처럼 두 테이블을 결합하지 않고, "일치하는 행이 있는가"만 확인하고 바깥 행 하나만 남깁니다.

핵심

EXISTS 는 값을 가져오지 않고 존재만 확인하는 세미 조인입니다. JOIN 과 달리 매칭되는 행이 여러 개여도 바깥 행은 하나만 남습니다.

2.2 IN 과 EXISTS, 중복 제거 효과

IN (서브쿼리) 도 목록에 값이 있는지만 확인하므로 결과는 EXISTS 와 같은 경우가 많습니다. 서브쿼리가 같은 값을 여러 번 반환해도 바깥 행이 중복되지 않는다는 점도 같습니다.

이 "중복 없음"이 JOIN 과 다른 점입니다. emp 와 orders 를 JOIN 하면 주문이 2건인 직원은 2행으로 늘어나지만, EXISTS 나 IN 으로 "주문이 있는 직원"을 찾으면 몇 건을 주문했든 1행만 남습니다. 3절 예제에서 이 차이를 직접 비교합니다.

옵티마이저 수준에서는 IN 서브쿼리도 대부분 세미 조인으로 변환되어 실행되므로, 표준 문법 범위에서는 EXISTS 와 IN 의 성능 차이가 크지 않습니다. 차이가 커지는 지점은 서브쿼리 결과에 NULL 이 섞였을 때이며, 2.4절에서 다룹니다.

2.3 상관 EXISTS

바깥 쿼리의 컬럼을 EXISTS 안쪽 서브쿼리가 참조하면 상관 서브쿼리입니다. "취소된 주문이 있는 직원"처럼 바깥 행(직원)마다 조건이 달라지는 조회에 씁니다.

sql
WHERE EXISTS
      (SELECT 1
         FROM orders o
        WHERE o.emp_id = e.id
          AND o.status = '취소')

바깥 쿼리가 직원 한 명을 처리할 때마다 서브쿼리가 그 직원의 id 로 다시 실행됩니다. 상관 서브쿼리 일반의 특성(행마다 재실행)은 중급 01 의 2.4절에서 이미 다뤘으므로 여기서는 반복하지 않습니다.

2.4 NOT IN 과 3값 논리 함정

NOT IN (서브쿼리) 는 SQL 에서 가장 자주 오용되는 문법 중 하나입니다. 서브쿼리가 반환하는 목록에 NULL 이 하나라도 있으면, 전체 NOT IN 조건이 모든 행에서 UNKNOWN 이 되어 결과가 0행이 됩니다.

원리는 NOT IN 이 <> 값1 AND <> 값2 AND ... 의 줄임이라는 데 있습니다. 목록에 NULL 이 있으면 그 비교 <> NULL 은 참도 거짓도 아닌 UNKNOWN 이고, AND 로 묶인 조건 중 하나가 UNKNOWN 이면 다른 조건이 모두 참이어도 전체가 UNKNOWN 이 됩니다. WHERE 절은 UNKNOWN 을 참으로 보지 않으므로 그 행은 걸러집니다.

주의

NOT IN 서브쿼리에 NULL 이 하나라도 섞이면 일부 행이 아니라 전체 결과가 0행이 됩니다. "값이 이상해서 몇 건만 빠졌겠지"가 아니라 "전부 사라졌다"가 이 함정의 증상입니다.

2.5 안전한 대안 세 가지

NOT IN 함정은 아래 세 가지 방법으로 피할 수 있습니다.

방법 핵심 주의점
NOT EXISTS 상관 서브쿼리로 존재 확인 서브쿼리에 조인 조건 필수
NOT IN + IS NOT NULL 서브쿼리에서 NULL 을 미리 제거 서브쿼리 컬럼에만 적용, 바깥 컬럼은 별개
LEFT JOIN ... IS NULL 안티 조인으로 매칭 안 된 행만 남김 조인 키가 NULL 이면 매칭 자체가 안 됨

NOT EXISTS 는 NULL 비교 자체를 하지 않고 "일치하는 행이 있는가"만 보므로 서브쿼리에 NULL 이 있어도 영향이 없습니다. 실무에서는 세 방법 중 NOT EXISTS 를 기본으로 권장합니다. 의미가 가장 분명하고 NULL 로 인한 부작용이 없기 때문입니다.

2.6 성능 메모: 세미 조인 변환과 단락 평가

옵티마이저는 EXISTS·IN·NOT EXISTS·NOT IN 을 실행 계획 단계에서 세미 조인(semi join) 또는 안티 조인(anti join) 이라는 내부 조인 방식으로 바꿔 처리합니다. 아래는 그 형태를 보여주는 설명용 실행 계획입니다. H2 는 이런 계획을 이 형태로 보여주지 않아 실행하지 않았습니다.

text
Oracle 실행 계획(설명용, 실행 안 함)
--------------------------------
NESTED LOOPS SEMI
  TABLE ACCESS FULL  EMP
  TABLE ACCESS FULL  ORDERS

NESTED LOOPS SEMI 는 EMP 한 행마다 ORDERS 에서 일치하는 행을 찾다가 첫 행을 찾으면 그 즉시 다음 EMP 행으로 넘어갑니다. 이것이 "EXISTS 는 첫 행을 찾으면 더 뒤지지 않고 멈춘다"는 단락 평가(short-circuit)이며, 대상 테이블이 클수록 EXISTS 가 유리해지는 이유입니다.

IN 의 목록을 서브쿼리 대신 리터럴로 직접 나열할 때는 개수 제한도 있습니다. Oracle 은 IN (1, 2, 3, ...) 처럼 괄호 안에 콤마로 나열하는 리터럴이 1000개를 넘으면 ORA-01795 오류가 납니다. 리스트가 길어질 가능성이 있는 코드는 임시 테이블에 값을 넣고 IN (서브쿼리) 로 바꾸는 편이 안전합니다.

팁

IN 리터럴이 1000개를 넘을 위험이 있으면 처음부터 임시 테이블이나 IN (서브쿼리) 형태로 설계합니다. 나중에 데이터가 늘어난 뒤 고치면 운영 중 오류로 발견하게 됩니다.

핵심 원리
  • 2.1 EXISTS 와 세미 조인
  • 2.2 IN 과 EXISTS, 중복 제거 효과
  • 2.3 상관 EXISTS
  • 2.4 NOT IN 과 3값 논리 함정
  • 2.5 안전한 대안 세 가지
  • 2.6 성능 메모: 세미 조인 변환과 단락 평가
이전 섹션1 왜 배우는가2 / 6다음 섹션3 코드 예제