공공부하자개발 · 영어 학습 노트
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정리‹ 이전다음 ›

4. 응용 변형 예제

소스: sql-src/mid_02_exists_in/02_not_in_null.sql, 03_dialects.sql. NOT IN 함정을 재현하고, 세 가지 해결법과 DB별 차이를 비교합니다.

변형 1: 3값 논리 확인

sql
SELECT
       CASE WHEN 1 IN (2, NULL) THEN 'TRUE'
            WHEN NOT (1 IN (2, NULL)) THEN 'FALSE'
            ELSE 'UNKNOWN'
       END AS logic_check;
text
LOGIC_CHECK
-----------
UNKNOWN
(1행)

1 은 2 와도 NULL 과도 같지 않지만, IN 도 NOT 도 참이 되지 못하고 UNKNOWN 이 나옵니다. NOT IN 함정은 이 UNKNOWN 이 WHERE 절에서 통째로 걸러지며 생깁니다.

변형 2: NOT IN 함정 재현

sql
SELECT
       e.name
     , e.dept
  FROM emp e
 WHERE e.id NOT IN (SELECT o.emp_id FROM orders o)
 ORDER BY e.id;
text
NAME | DEPT
-----+-----
(0행)

"주문이 없는 직원"은 실제로 7명(1, 4, 6~10번)인데 0행이 나왔습니다. orders.emp_id 목록에 106번 주문의 NULL 이 포함돼, 모든 직원 행에서 e.id NOT IN (...) 이 UNKNOWN 이 되기 때문입니다.

변형 3: 해결 1 — NOT EXISTS

sql
SELECT
       e.name
     , e.dept
  FROM emp e
 WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.emp_id = e.id)
 ORDER BY e.id;
text
NAME   | DEPT
-------+-----
김대표 | 경영
최사원 | 개발
서팀장 | 인사
강팀장 | 지원
한주임 | 영업
문사원 | 인사
오사원 | 지원
(7행)

의도한 7행이 정확히 나옵니다. NOT EXISTS 는 NULL 과 값을 비교하지 않고 "일치하는 행이 있는가"만 확인하므로 서브쿼리의 NULL 에 영향받지 않습니다.

변형 4: 해결 2 — NOT IN 서브쿼리에 IS NOT NULL 추가

sql
SELECT
       e.name
     , e.dept
  FROM emp e
 WHERE e.id NOT IN
       (SELECT o.emp_id
          FROM orders o
         WHERE o.emp_id IS NOT NULL)
 ORDER BY e.id;
text
NAME   | DEPT
-------+-----
김대표 | 경영
최사원 | 개발
서팀장 | 인사
강팀장 | 지원
한주임 | 영업
문사원 | 인사
오사원 | 지원
(7행)

같은 7행입니다. 서브쿼리 안에서 NULL 을 미리 걸러 목록에 아예 들어가지 않게 했습니다. NOT IN 을 꼭 써야 하는 상황이라면 이 조건을 습관처럼 붙입니다.

변형 5: 해결 3 — LEFT JOIN 안티 조인

sql
SELECT
       e.name
     , e.dept
  FROM emp e
  LEFT JOIN orders o ON o.emp_id = e.id
 WHERE o.id IS NULL
 ORDER BY e.id;
text
NAME   | DEPT
-------+-----
김대표 | 경영
최사원 | 개발
서팀장 | 인사
강팀장 | 지원
한주임 | 영업
문사원 | 인사
오사원 | 지원
(7행)

역시 같은 7행입니다. emp 를 orders 에 LEFT JOIN 하면 주문이 없는 직원은 o.* 가 모두 NULL 로 채워지고, WHERE o.id IS NULL 로 그 행만 남깁니다. 세 방법 모두 결과가 같으므로 팀 컨벤션에 맞는 것을 고르되, NOT IN 은 피하는 편이 안전합니다.

변형 6: DB별 차이

기능 Oracle · Tibero MySQL MSSQL
빈 문자열 '' NULL 로 취급 빈 문자열 그대로 빈 문자열 그대로
NOT IN + NULL 함정 '' 로 더 자주 발생 값 자체가 NULL 일 때만 표준과 동일
IN 서브쿼리 최적화 세미 조인 변환 5.5 이하 구버전 이슈 세미 조인 변환
sql
-- Oracle · Tibero
SET MODE Oracle;
CREATE TABLE skip_dept (code VARCHAR2(10));
INSERT INTO skip_dept VALUES
  ('지원'),
  ('');
SELECT
       code
     , code IS NULL AS is_null
  FROM skip_dept
 ORDER BY code;
text
CODE | IS_NULL
-----+--------
NULL | true
지원 | false
(2행)

빈 문자열로 넣은 행이 실제로는 NULL 로 저장됐습니다. Oracle(과 Oracle 호환인 Tibero)은 VARCHAR2 에 들어간 '' 를 NULL 과 같게 취급합니다. IS NULL 이 참으로 나온 이유입니다. H2 를 Oracle 모드로 돌려 직접 재현했습니다.

sql
SELECT
       d.code
     , d.dname
  FROM dept d
 WHERE d.code NOT IN (SELECT code FROM skip_dept)
 ORDER BY d.code;
text
CODE | DNAME
-----+------
(0행)

'지원' 부서 하나만 빼려고 만든 목록인데, '' 가 NULL 로 바뀌면서 NOT IN 전체가 UNKNOWN 이 되어 0행입니다. 개발 DB(H2·MySQL)에서 '' 를 넣고 테스트할 때는 통과했다가 Oracle 운영 DB에 배포한 뒤에야 드러나는 전형적인 사고 패턴입니다.

주의

Oracle 계열은 빈 문자열과 NULL 을 구분하지 않습니다. "값이 없으면 빈 문자열을 넣는다"는 습관은 Oracle 에서 NOT IN 함정을 더 자주 일으킵니다. 애초에 NULL 을 그대로 쓰고 NOT EXISTS 로 확인하는 편이 안전합니다.

sql
-- MySQL
SET MODE MySQL;
CREATE TABLE skip_dept2 (code VARCHAR(10));
INSERT INTO skip_dept2 VALUES
  ('지원'),
  ('');
SELECT
       code
     , code IS NULL AS is_null
  FROM skip_dept2
 ORDER BY code;
text
CODE | IS_NULL
-----+--------
     | false
지원 | false
(2행)

MySQL 은 '' 를 그대로 빈 문자열로 저장합니다. IS NULL 이 둘 다 false 로, Oracle 과 반대입니다. MySQL 에서 NOT IN 함정은 실제 NULL 값이 섞였을 때만 생깁니다.

sql
-- MSSQL
SET MODE MSSQLServer;
SELECT
       e.name
  FROM emp e
 WHERE e.id NOT IN (SELECT o.emp_id FROM orders o)
 ORDER BY e.id;
text
NAME
----
(0행)

MSSQL 도 빈 문자열을 NULL 로 바꾸지 않지만, orders.emp_id 처럼 실제 NULL 값이 있는 목록에 NOT IN 을 쓰면 표준과 똑같이 0행이 됩니다. MSSQL 이 다른 부분은 '' 처리뿐이고, NOT IN 의 3값 논리 자체는 Oracle·MySQL·MSSQL 모두 표준과 동일합니다.

MySQL 5.5 이하 버전은 IN (서브쿼리) 를 세미 조인으로 바꾸지 못하고 상관 EXISTS 로 고쳐 실행했습니다. 실행 계획에 DEPENDENT SUBQUERY 로 나오며, 바깥 테이블 행마다 서브쿼리를 다시 실행해 바깥 테이블이 크면 매우 느렸습니다. MySQL 5.6 부터 세미 조인 최적화가 들어가 해소됐습니다. 지금은 오래된 버전을 만났을 때만 참고할 이력입니다.

응용 변형 예제
  • 변형 1: 3값 논리 확인
  • 변형 2: NOT IN 함정 재현
  • 변형 3: 해결 1 — NOT EXISTS
  • 변형 4: 해결 2 — NOT IN 서브쿼리에 IS NOT NULL 추가
  • 변형 5: 해결 3 — LEFT JOIN 안티 조인
  • 변형 6: DB별 차이
이전 섹션3 코드 예제4 / 6다음 섹션5 자주 하는 실수 (Tip)