결론 먼저
SQLD 2과목에서 서브쿼리는 매회 3–4문제 고정으로 나옵니다. 위치별로 이름이 달라지는 게 핵심이고, 거기서 정답이 갈려요.
| 유형 | 위치 | 특징 |
|---|---|---|
| 스칼라 | SELECT 절 | 행 1개 + 컬럼 1개만 반환해야 함 |
| 인라인 뷰 | FROM 절 | 가상 테이블처럼 동작, ALIAS 필수 |
| 중첩 (Nested) | WHERE 절 | 단일행/다중행으로 나뉨 |
| 상관 (Correlated) | WHERE 절 | 메인 쿼리의 컬럼을 참조, 행마다 재실행 |
여기에 다중행 연산자(IN, ANY, ALL, EXISTS)와 NOT IN의 NULL 함정까지 합치면 SQLD 서브쿼리 출제 범위가 사실상 다 커버됩니다.
왜 서브쿼리에서 점수가 갈리나
JOIN처럼 그림으로 이해되는 영역이 아니어서 그래요. 위치만 바뀌어도 이름이 바뀌고, 결과 행 수가 안 맞으면 바로 오류가 납니다.
특히 "이 서브쿼리는 몇 행을 반환하는가?" 만 머릿속에 정확히 그려져도 절반은 풀려요. SQLD에서 서브쿼리 문제는 결과 예측 + 함정 인식이 거의 전부입니다.
서브쿼리는 위치(SELECT/FROM/WHERE) → 반환 행 수 → 메인 쿼리 컬럼 참조 여부, 이 3가지 축으로만 정리하면 됩니다.1. 스칼라 서브쿼리 (SELECT 절)
SELECT 컬럼 자리에 들어가는 서브쿼리. 행 1개 + 컬럼 1개만 반환해야 합니다.
SELECT 사번,
이름,
(SELECT 부서명 FROM 부서 d WHERE d.부서코드 = e.부서코드) AS 부서명
FROM 직원 e;
체크 포인트:
- 결과가 2행 이상이면 오류 (
single-row subquery returns more than one row) - 결과가 0행이면 NULL 반환 (오류 X) — 자주 틀리는 부분
- 결과가 2컬럼 이상이면 오류
JOIN으로 충분히 대체 가능하지만, "이 컬럼 하나만 더 붙이고 싶다" 는 의도를 명확히 드러낼 때 씁니다.
2. 인라인 뷰 (FROM 절)
FROM 자리에 들어가는 서브쿼리. 가상 테이블로 취급되고 ALIAS가 거의 필수예요.
SELECT d.부서명, t.평균급여
FROM (SELECT 부서코드, AVG(급여) AS 평균급여
FROM 직원
GROUP BY 부서코드) t
JOIN 부서 d ON d.부서코드 = t.부서코드;
체크 포인트:
- 메인 쿼리의 컬럼 참조 불가 (참조하면 오류)
- 그룹화한 결과를 다시 조인할 때 가장 많이 씀
- TOP-N 쿼리(ROWNUM 활용)에서도 단골
SELECT *
FROM (SELECT * FROM 직원 ORDER BY 급여 DESC)
WHERE ROWNUM <= 5;
위처럼 인라인 뷰 안에서 정렬 → ROWNUM으로 자르기가 SQLD 출제 단골 패턴입니다.
3. 중첩 서브쿼리 (WHERE 절, 비상관)
WHERE 자리에 들어가고, 메인 쿼리 컬럼을 참조하지 않는 서브쿼리. 한 번 실행 후 결과를 메인 쿼리에서 사용해요.
단일행 서브쿼리
비교 연산자(=, >, <, >=, <=)와 함께. 결과가 2행 이상이면 오류.
SELECT 이름, 급여
FROM 직원
WHERE 급여 > (SELECT AVG(급여) FROM 직원);
다중행 서브쿼리
결과가 여러 행일 때 사용하는 연산자:
| 연산자 | 의미 | 예시 |
|---|---|---|
IN | 결과 집합 중 하나라도 일치 | WHERE 부서코드 IN (SELECT ...) |
ANY, SOME | 결과 중 하나라도 조건 만족 | > ANY(...) = 최소값보다 크면 됨 |
ALL | 결과 모두 조건 만족 | > ALL(...) = 최대값보다 커야 함 |
EXISTS | 결과 행이 1개라도 있으면 TRUE | WHERE EXISTS (SELECT 1 ...) |
> ANY vs > ALL은 매번 헷갈리는데, ANY = 가장 작은 값보다, ALL = 가장 큰 값보다 만 외우면 됩니다.
4. 상관 서브쿼리 (Correlated)
WHERE 절 서브쿼리 중에서 메인 쿼리의 컬럼을 참조하는 형태. 메인 쿼리의 행마다 서브쿼리가 다시 실행돼요.
SELECT 이름, 급여
FROM 직원 e
WHERE 급여 > (SELECT AVG(급여)
FROM 직원
WHERE 부서코드 = e.부서코드);
→ "각 직원이 자기 부서 평균보다 많이 받는가?" 를 묻는 쿼리입니다.
체크 포인트:
- 메인 쿼리의 alias(
e)를 서브쿼리 안에서 참조 - 행 수만큼 서브쿼리 반복 실행 → 인덱스 없으면 느려짐
- SQLD에서는 "실행 횟수" 를 묻는 문제도 가끔 나옴
비상관 서브쿼리는 1번만, 상관 서브쿼리는 메인 쿼리 행 수만큼 실행된다는 점이 핵심.
IN vs EXISTS 차이
비슷해 보이지만 동작 방식이 다릅니다.
-- IN
SELECT 사번 FROM 직원
WHERE 부서코드 IN (SELECT 부서코드 FROM 부서 WHERE 위치 = '서울');
-- EXISTS (상관 서브쿼리)
SELECT 사번 FROM 직원 e
WHERE EXISTS (SELECT 1 FROM 부서 d
WHERE d.부서코드 = e.부서코드 AND d.위치 = '서울');
| 항목 | IN | EXISTS |
|---|---|---|
| 서브쿼리 결과 | 값 집합 전체를 반환 | 행 존재 여부만 확인 (TRUE/FALSE) |
| NULL 처리 | 결과 집합에 NULL 포함 시 함정 | NULL 영향 없음 |
| 성능 | 결과 집합이 작을 때 유리 | 메인 행이 적을 때 유리 |
→ "NULL 포함 가능성이 있으면 EXISTS" 만 기억하면 시험에서 안전합니다.
NOT IN의 NULL 함정 (매회 단골)
다중행 서브쿼리에서 가장 함정인 부분.
-- 직원 테이블에 NULL 부서코드가 있다면?
SELECT 사번 FROM 외부직원
WHERE 부서코드 NOT IN (SELECT 부서코드 FROM 부서);
-- → NULL 포함 시 결과가 항상 비어버립니다
NOT IN (..., NULL)은 SQL 3치 논리에서 항상 UNKNOWN이 되어서, 결과적으로 0행 반환돼요. 의도한 결과가 안 나옵니다.
해결법은 두 가지:
-- 방법 1: 서브쿼리에서 NULL 제외
WHERE 부서코드 NOT IN (SELECT 부서코드 FROM 부서 WHERE 부서코드 IS NOT NULL);
-- 방법 2: NOT EXISTS 사용 (권장)
WHERE NOT EXISTS (SELECT 1 FROM 부서 d WHERE d.부서코드 = e.부서코드);
시험에서 NOT IN 보면 NULL 함정 의심 1순위. 이 패턴은 매회 1문제는 무조건 출제됩니다.
NULL 관련 패턴은 SQLD NULL 함정 문제 완전 정복 글에서 더 깊이 다뤘어요.
자주 틀리는 5가지 패턴
1. 스칼라 서브쿼리에서 0행 반환 오류가 아니라 NULL이 반환됩니다. "오류"라고 헷갈리는 보기 자주 나와요.
2. 인라인 뷰에서 메인 컬럼 참조 시도 인라인 뷰는 비상관입니다. 메인 컬럼 참조하면 오류.
3. 단일행 서브쿼리에 다중행 결과
= (SELECT ...)인데 결과가 2행 이상이면 런타임 오류. 비교 연산자 + 다중행은 절대 안 됨.
4. > ANY vs > ALL 혼동
> ANY(10, 20, 30)→ 10보다 크면 됨 (가장 작은 값 기준)> ALL(10, 20, 30)→ 30보다 커야 함 (가장 큰 값 기준)
5. NOT IN + NULL → 0행 위에서 설명. 항상 의심.
시험장에서의 접근법
- 위치부터 식별: SELECT(스칼라) / FROM(인라인) / WHERE(중첩·상관)
- 반환 행 수 확인: 1행 / N행 / 빈 결과
- 메인 컬럼 참조 여부: 있으면 상관, 없으면 비상관
- NULL이 보이면 NOT IN 가드 의심
> ANY/ALL나오면 머릿속으로 최소/최대값 대입
이 5단계만 기계적으로 적용하면 서브쿼리 4문제 중 3.5문제는 맞출 수 있어요.
시간이 부족하면 서브쿼리는 우선순위 상위로 풀어두세요. JOIN보다 함정이 정형화되어 있어 점수 회수율이 높습니다.정리
- 위치 → 이름 → 동작: 서브쿼리는 위치만 바뀌어도 이름이 바뀜
- 스칼라: 1행 1컬럼 강제, 0행이면 NULL
- 인라인 뷰: ALIAS 필수, 메인 컬럼 참조 불가
- 중첩(비상관): 1번 실행, 다중행이면 IN/ANY/ALL/EXISTS
- 상관: 메인 행마다 재실행, 컬럼 참조 가능
- NOT IN + NULL: 항상 0행 → NOT EXISTS로 회피