EXISTS (서브쿼리) 는 서브쿼리가 결과를 한 행이라도 반환하면 참입니다. 값이 아니라 존재 여부만 보므로, 서브쿼리의 SELECT 목록에 무엇을 쓰든 결과에 영향이 없어 관례상 SELECT 1 을 씁니다.
바깥 쿼리 한 행과 서브쿼리가 조건으로 연결되는 방식이어서, EXISTS 는 개념적으로 "세미 조인(semi join)"입니다. 일반 JOIN 처럼 두 테이블을 결합하지 않고, "일치하는 행이 있는가"만 확인하고 바깥 행 하나만 남깁니다.
핵심EXISTS 는 값을 가져오지 않고 존재만 확인하는 세미 조인입니다. JOIN 과 달리 매칭되는 행이 여러 개여도 바깥 행은 하나만 남습니다.
IN (서브쿼리) 도 목록에 값이 있는지만 확인하므로 결과는 EXISTS 와 같은 경우가 많습니다. 서브쿼리가 같은 값을 여러 번 반환해도 바깥 행이 중복되지 않는다는 점도 같습니다.
이 "중복 없음"이 JOIN 과 다른 점입니다. emp 와 orders 를 JOIN 하면 주문이 2건인 직원은 2행으로 늘어나지만, EXISTS 나 IN 으로 "주문이 있는 직원"을 찾으면 몇 건을 주문했든 1행만 남습니다. 3절 예제에서 이 차이를 직접 비교합니다.
옵티마이저 수준에서는 IN 서브쿼리도 대부분 세미 조인으로 변환되어 실행되므로, 표준 문법 범위에서는 EXISTS 와 IN 의 성능 차이가 크지 않습니다. 차이가 커지는 지점은 서브쿼리 결과에 NULL 이 섞였을 때이며, 2.4절에서 다룹니다.
바깥 쿼리의 컬럼을 EXISTS 안쪽 서브쿼리가 참조하면 상관 서브쿼리입니다. "취소된 주문이 있는 직원"처럼 바깥 행(직원)마다 조건이 달라지는 조회에 씁니다.
WHERE EXISTS
(SELECT 1
FROM orders o
WHERE o.emp_id = e.id
AND o.status = '취소')바깥 쿼리가 직원 한 명을 처리할 때마다 서브쿼리가 그 직원의 id 로 다시 실행됩니다. 상관 서브쿼리 일반의 특성(행마다 재실행)은 중급 01 의 2.4절에서 이미 다뤘으므로 여기서는 반복하지 않습니다.
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행이 됩니다. "값이 이상해서 몇 건만 빠졌겠지"가 아니라 "전부 사라졌다"가 이 함정의 증상입니다.
NOT IN 함정은 아래 세 가지 방법으로 피할 수 있습니다.
| 방법 | 핵심 | 주의점 |
|---|---|---|
| NOT EXISTS | 상관 서브쿼리로 존재 확인 | 서브쿼리에 조인 조건 필수 |
| NOT IN + IS NOT NULL | 서브쿼리에서 NULL 을 미리 제거 | 서브쿼리 컬럼에만 적용, 바깥 컬럼은 별개 |
| LEFT JOIN ... IS NULL | 안티 조인으로 매칭 안 된 행만 남김 | 조인 키가 NULL 이면 매칭 자체가 안 됨 |
NOT EXISTS 는 NULL 비교 자체를 하지 않고 "일치하는 행이 있는가"만 보므로 서브쿼리에 NULL 이 있어도 영향이 없습니다. 실무에서는 세 방법 중 NOT EXISTS 를 기본으로 권장합니다. 의미가 가장 분명하고 NULL 로 인한 부작용이 없기 때문입니다.
옵티마이저는 EXISTS·IN·NOT EXISTS·NOT IN 을 실행 계획 단계에서 세미 조인(semi join) 또는 안티 조인(anti join) 이라는 내부 조인 방식으로 바꿔 처리합니다. 아래는 그 형태를 보여주는 설명용 실행 계획입니다. H2 는 이런 계획을 이 형태로 보여주지 않아 실행하지 않았습니다.
Oracle 실행 계획(설명용, 실행 안 함)
--------------------------------
NESTED LOOPS SEMI
TABLE ACCESS FULL EMP
TABLE ACCESS FULL ORDERSNESTED LOOPS SEMI 는 EMP 한 행마다 ORDERS 에서 일치하는 행을 찾다가 첫 행을 찾으면 그 즉시 다음 EMP 행으로 넘어갑니다. 이것이 "EXISTS 는 첫 행을 찾으면 더 뒤지지 않고 멈춘다"는 단락 평가(short-circuit)이며, 대상 테이블이 클수록 EXISTS 가 유리해지는 이유입니다.
IN 의 목록을 서브쿼리 대신 리터럴로 직접 나열할 때는 개수 제한도 있습니다. Oracle 은 IN (1, 2, 3, ...) 처럼 괄호 안에 콤마로 나열하는 리터럴이 1000개를 넘으면 ORA-01795 오류가 납니다. 리스트가 길어질 가능성이 있는 코드는 임시 테이블에 값을 넣고 IN (서브쿼리) 로 바꾸는 편이 안전합니다.
팁IN 리터럴이 1000개를 넘을 위험이 있으면 처음부터 임시 테이블이나 IN (서브쿼리) 형태로 설계합니다. 나중에 데이터가 늘어난 뒤 고치면 운영 중 오류로 발견하게 됩니다.