서브쿼리 종류와 NOT IN의 NULL 함정
목차
SQLD 2과목의 「서브 쿼리」 항목을 정리합니다. 서브쿼리를 위치·반환 형태·메인쿼리와의 관계로 나누는 분류와 공통 규칙, 스칼라 서브쿼리와 인라인 뷰, 단일행·다중행 서브쿼리와 IN·ANY·ALL, EXISTS와 NOT EXISTS, 서브쿼리 결과에 NULL이 섞였을 때 NOT IN이 0행이 되는 함정, 다중컬럼 서브쿼리와 연관 서브쿼리, 뷰의 특징, SQL Server와의 차이를 쇼핑몰 예시로 살펴본 뒤 기출 유형 연습 문제 5개로 확인합니다.
앞 글 「표준 조인, INNER·OUTER·CROSS JOIN」에서 여러 테이블을 FROM 절에서 잇는 방법을 정리했습니다. 이번에는 SQL 문 안에 또 하나의 SELECT 문을 넣는 서브쿼리를 다룹니다. 서브쿼리는 조인과 같은 질문을 다른 방법으로 풀 때가 많아서, 앞의 두 글에서 본 「주문이 없는 고객」, 「주문이 없는 상품」 같은 예를 다시 씁니다. 시험에서는 서브쿼리 종류에 대한 설명 중 틀린 것을 고르는 문제와, NOT IN에 NULL이 섞였을 때의 결과를 묻는 문제가 자주 나옵니다. 예시 데이터는 앞 글들과 같은 쇼핑몰 테이블과 가격대, 쿠폰 테이블입니다. 연습 문제는 실제 시험과 같은 유형으로 새로 만든 문제입니다.
1. 서브쿼리의 종류와 공통 규칙

서브쿼리(Subquery)는 SQL 문 안에 들어 있는 SELECT 문입니다. 서브쿼리를 품은 바깥 SQL은 메인쿼리라고 부릅니다.
세 가지 기준으로 나누기
| 기준 | 종류 | 뜻 |
|---|---|---|
| 위치 | 스칼라 서브쿼리 | SELECT 절 등에서 값 하나(1행 1열)를 돌려줌 |
| 위치 | 인라인 뷰 | FROM 절의 서브쿼리. 테이블처럼 씀 |
| 위치 | 중첩 서브쿼리 | WHERE 절이나 HAVING 절의 서브쿼리 |
| 반환 형태 | 단일행 | 결과가 1행. =, <>, >, >=, <, <=와 함께 씀 |
| 반환 형태 | 다중행 | 결과가 여러 행. IN, ANY(SOME), ALL, EXISTS와 함께 씀 |
| 반환 형태 | 다중컬럼 | 결과가 여러 컬럼. (a, b) IN (...)처럼 씀 |
| 메인쿼리와의 관계 | 비연관 서브쿼리 | 메인쿼리 컬럼을 쓰지 않음. 혼자 실행 가능 |
| 메인쿼리와의 관계 | 연관 서브쿼리 | 메인쿼리 컬럼을 씀. 메인쿼리 행마다 다시 계산한다고 이해 |
수험서는 서브쿼리를 이 세 기준(위치, 반환 형태, 동작 방식)으로 나눠 설명합니다. 한 서브쿼리가 여러 기준에 동시에 해당할 수 있습니다. 예를 들어 WHERE 절에서 IN과 함께 쓴 서브쿼리는 중첩 서브쿼리이면서 다중행 서브쿼리입니다.
공통 규칙
| 규칙 | 결과 |
|---|---|
| 서브쿼리는 괄호로 감쌈 | (SELECT ...) |
| 메인쿼리는 서브쿼리 안의 컬럼을 쓸 수 없음 | 쓰면 오류(ORA-00904) |
| 서브쿼리는 메인쿼리의 컬럼을 쓸 수 있음 | 연관 서브쿼리가 됨 |
| WHERE 절 서브쿼리에는 ORDER BY를 쓰지 않음 | 쓰면 오류(ORA-00907) |
| FROM 절 인라인 뷰에는 ORDER BY를 쓸 수 있음 | Top N 쿼리에 씀 |
-- 오류(ORA-00907): WHERE 절 서브쿼리 안의 ORDER BY
SELECT 상품번호 FROM 상품
WHERE 상품번호 IN (SELECT 상품번호 FROM 주문상품 ORDER BY 상품번호);
-- 실행됨: 인라인 뷰 안의 ORDER BY(판매가 상위 2개)
SELECT *
FROM (SELECT 상품번호, 판매가 FROM 상품 ORDER BY 판매가 DESC, 상품번호)
WHERE ROWNUM <= 2;
두 번째 SQL의 결과는 P60 25000, P40 5000입니다. 「WHERE 절과 ORDER BY, Top N 쿼리」에서 본 ROWNUM Top N이 바로 인라인 뷰에서 정렬하는 방법입니다. 「서브쿼리에는 ORDER BY를 쓸 수 없다」는 설명은 WHERE 절 서브쿼리에 해당하는 규칙으로 읽으면 됩니다.
2. 스칼라 서브쿼리

스칼라 서브쿼리는 값 하나(1행 1열)를 돌려주는 서브쿼리로, 주로 SELECT 절에서 컬럼처럼 씁니다.
SELECT c.고객번호,
(SELECT COUNT(*) FROM 주문 o WHERE o.고객번호 = c.고객번호) AS 주문건수,
(SELECT k.쿠폰명 FROM 쿠폰 k WHERE k.고객번호 = c.고객번호) AS 쿠폰명
FROM 고객 c
ORDER BY c.고객번호;
| 고객번호 | 주문건수 | 쿠폰명 |
|---|---|---|
| C01 | 2 | 여름_10%할인 |
| C02 | 2 | NULL |
| C03 | 2 | 여름10할인 |
| C04 | 1 | NULL |
| C05 | 0 | NULL |
- 서브쿼리 결과가 0행이면 NULL입니다. C02, C04, C05는 쿠폰이 없어 쿠폰명이 NULL입니다.
- COUNT는 대상 행이 없어도 0을 돌려주므로 C05의 주문건수는 0입니다.
- 짝이 없는 고객도 메인쿼리 행이 그대로 남으므로 결과가 LEFT OUTER JOIN과 비슷합니다.
| 잘못 쓴 예 | 결과 |
|---|---|
| 고객마다 주문번호를 가져옴(주문이 2건인 고객이 있음) | 오류(ORA-01427). 단일행 서브쿼리가 여러 행을 돌려줌 |
| 컬럼 두 개(주문번호, 주문일시)를 가져옴 | 오류(ORA-00913). 값이 너무 많음 |
스칼라 서브쿼리는 ORDER BY 절에도 쓸 수 있습니다. ORDER BY (SELECT COUNT(*) FROM 주문 o WHERE o.고객번호 = c.고객번호) DESC, c.고객번호로 고객을 정렬하면 C01, C02, C03, C04, C05 순서입니다.
3. 인라인 뷰

인라인 뷰는 FROM 절에 쓴 서브쿼리입니다. 서브쿼리 결과를 임시 테이블처럼 쓰고, 다른 테이블과 조인할 수도 있습니다.
SELECT o.주문번호, c.고객명, v.주문금액
FROM 주문 o
JOIN 고객 c ON o.고객번호 = c.고객번호
JOIN (SELECT 주문번호, SUM(주문수량 * 주문단가) AS 주문금액
FROM 주문상품
GROUP BY 주문번호) v
ON o.주문번호 = v.주문번호
ORDER BY o.주문번호;
| 주문번호 | 고객명 | 주문금액 |
|---|---|---|
| 5001 | 김하나 | 10000 |
| 5002 | 이두리 | 3000 |
| 5003 | 김하나 | 6000 |
| 5004 | 박세미 | 12000 |
| 5005 | 이두리 | 8000 |
| 5006 | 최네오 | 8000 |
| 5007 | 박세미 | 29000 |
- 인라인 뷰 안에서 만든 별칭(주문금액)은 바깥 쿼리의 WHERE에서 쓸 수 있습니다.
WHERE v.주문금액 >= 10000을 더하면 5001, 5004, 5007이 남습니다. - 「SELECT 문 기초와 관계형 데이터베이스」에서 본 「WHERE에서는 SELECT 별칭을 쓸 수 없다」는 규칙을 피하는 방법이기도 합니다. 계산은 인라인 뷰 안에서 하고, 거르는 것은 바깥에서 합니다.
- 같은 계산을 WITH 절로 이름을 붙여 쓸 수도 있습니다.
WITH 주문금액표 AS (...)로 위 인라인 뷰에 이름을 붙이고SELECT MAX(주문금액), MIN(주문금액) FROM 주문금액표를 실행하면 29000, 3000입니다.
4. 단일행·다중행 서브쿼리

단일행 서브쿼리
결과가 1행인 서브쿼리는 =, > 같은 단일행 비교 연산자와 함께 씁니다.
-- 배(4000)보다 비싸거나 같은 상품: P20 배, P40 귤, P50 청사과, P60 배 선물세트
SELECT 상품번호, 상품명 FROM 상품
WHERE 판매가 >= (SELECT 판매가 FROM 상품 WHERE 상품명 = '배');
-- 가장 최근 주문: 5007(C03)
SELECT 주문번호, 고객번호 FROM 주문
WHERE 주문일시 = (SELECT MAX(주문일시) FROM 주문);
| 경우 | 결과 |
|---|---|
| 평균 판매가(약 7166.67)보다 비싼 상품 | P60 배 선물세트 |
| 서브쿼리가 2행(상품명 LIKE '%사과'는 사과·청사과) | 오류(ORA-01427) |
| 서브쿼리가 0행(상품명 = '포도') | 결과 0행(오류 아님) |
단일행 연산자 뒤의 서브쿼리가 여러 행을 돌려주면 오류이고, 0행이면 비교할 값이 NULL이 되어 결과가 0행입니다.
HAVING 절에도 서브쿼리를 쓸 수 있습니다. 상품별 총판매수량이 상품당 평균 판매수량(18 ÷ 5 = 3.6)보다 큰 상품을 구해 보겠습니다.
SELECT 상품번호, SUM(주문수량) AS 총수량
FROM 주문상품
GROUP BY 상품번호
HAVING SUM(주문수량) > (SELECT SUM(주문수량) / COUNT(DISTINCT 상품번호) FROM 주문상품)
ORDER BY 상품번호;
결과는 P10 5개, P30 6개입니다.
다중행 서브쿼리: IN, ANY, ALL
결과가 여러 행이면 IN, ANY(SOME), ALL과 함께 씁니다. 기준 서브쿼리는 SELECT 판매가 FROM 상품 WHERE 상품명 LIKE '%사과'이고, 결과는 사과 3000, 청사과 4000입니다.
| 조건 | 뜻 | 결과 |
|---|---|---|
IN (...), = ANY (...) |
3000 또는 4000과 같음 | P10, P20, P50 |
> ANY (...) |
가장 작은 값(3000)보다 큼 | P20, P40, P50, P60 |
> ALL (...) |
가장 큰 값(4000)보다 큼 | P40, P60 |
< ANY (...) |
가장 큰 값(4000)보다 작음 | P10, P30 |
< ALL (...) |
가장 작은 값(3000)보다 작음 | P30 |
> ANY는 「하나라도 넘으면」이라 최솟값만 넘으면 되고,> ALL은 「전부 넘어야」 하므로 최댓값을 넘어야 합니다. SOME은 ANY와 같습니다.- 서브쿼리가 0행이면
> ALL은 참이라 상품 6행이 모두 나오고,> ANY는 거짓이라 0행입니다. WHERE 상품번호 IN (SELECT 상품번호 FROM 주문상품)은 주문된 상품 5개(P40 제외)가 한 번씩 나옵니다. 같은 내용을 조인으로 쓰면 주문상품 줄 수만큼 11행이 나옵니다. IN과 EXISTS는 「있는지」만 보므로 메인쿼리 행이 늘어나지 않습니다.
5. EXISTS와 NOT EXISTS

EXISTS는 서브쿼리가 한 행이라도 돌려주는지만 봅니다.
SELECT c.고객번호, c.고객명
FROM 고객 c
WHERE NOT EXISTS (SELECT 1 FROM 주문 o WHERE o.고객번호 = c.고객번호);
- 결과는 C05 정다솜, 즉 주문이 없는 고객입니다. NOT EXISTS를 EXISTS로 바꾸면 C01~C04 네 명입니다.
- 서브쿼리의 SELECT 목록은 결과에 영향을 주지 않습니다.
SELECT 1,SELECT *,SELECT NULL모두 EXISTS 결과가 4명으로 같습니다. - EXISTS는 보통 위처럼 메인쿼리 컬럼을 쓰는 연관 서브쿼리로 씁니다. 메인쿼리 컬럼을 쓰지 않아도 문법 오류는 아니지만, 그때는 서브쿼리 결과가 모든 행에서 같습니다. 예를 들어
WHERE EXISTS (SELECT 1 FROM 주문 o WHERE o.주문번호 = 5001)은 서브쿼리가 늘 1행이라 고객 5명이 모두 나옵니다.
6. NOT IN과 NULL 함정

「쿠폰을 받지 않은 고객」을 구해 보겠습니다. 쿠폰 테이블의 고객번호는 C01, C03, NULL(전체 고객용 K03)입니다.
SELECT 고객번호 FROM 고객
WHERE 고객번호 NOT IN (SELECT 고객번호 FROM 쿠폰);
C02, C04, C05가 나올 것 같지만 결과는 0행입니다.
| 조건 | 결과 |
|---|---|
IN (쿠폰의 고객번호) |
C01, C03 |
NOT IN (쿠폰의 고객번호) |
0행 |
NOT IN, 서브쿼리에 IS NOT NULL 추가 |
C02, C04, C05 |
NOT EXISTS 연관 서브쿼리 |
C02, C04, C05 |
'C02' NOT IN ('C01', 'C03', NULL)은'C02' <> 'C01' AND 'C02' <> 'C03' AND 'C02' <> NULL입니다. 마지막 비교가 UNKNOWN이라 전체가 TRUE가 될 수 없습니다. 「WHERE 절과 ORDER BY, Top N 쿼리」에서 본NOT IN ('G', NULL)이 0행인 것과 같은 원리이고, 목록이 서브쿼리 결과로 바뀌었을 뿐입니다.- IN은
= 'C01' OR = 'C03' OR = NULL이라 하나만 참이면 되므로 NULL이 있어도 문제가 없습니다. - NOT EXISTS는 NULL과의 비교를 「짝 없음」으로 처리하므로 의도대로 C02, C04, C05가 나옵니다.
바르게 고친 두 가지 SQL입니다.
-- 방법 1: 서브쿼리에서 NULL을 뺀다
SELECT 고객번호 FROM 고객
WHERE 고객번호 NOT IN (SELECT 고객번호 FROM 쿠폰 WHERE 고객번호 IS NOT NULL);
-- 방법 2: NOT EXISTS로 바꾼다
SELECT c.고객번호 FROM 고객 c
WHERE NOT EXISTS (SELECT 1 FROM 쿠폰 k WHERE k.고객번호 = c.고객번호);
이 함정은 SQL Server에서도 똑같이 0행입니다. DBMS의 차이가 아니라 NULL과 3치 논리 때문입니다.
7. 다중컬럼 서브쿼리와 연관 서브쿼리

다중컬럼 서브쿼리
고객별 가장 최근 주문을 구합니다.
SELECT 주문번호, 고객번호, TO_CHAR(주문일시, 'MM-DD HH24:MI') AS 주문일시
FROM 주문
WHERE (고객번호, 주문일시) IN (SELECT 고객번호, MAX(주문일시)
FROM 주문
GROUP BY 고객번호)
ORDER BY 주문번호;
| 주문번호 | 고객번호 | 주문일시 |
|---|---|---|
| 5003 | C01 | 10-03 09:05 |
| 5005 | C02 | 10-05 20:40 |
| 5006 | C04 | 10-06 11:00 |
| 5007 | C03 | 10-07 16:45 |
- 주문일시만 비교하면 다른 고객의 최근 시각과 우연히 같은 주문도 걸릴 수 있으므로, 고객번호와 짝으로 비교합니다.
- 왼쪽 컬럼 수와 서브쿼리 컬럼 수가 다르면 오류입니다(ORA-00947).
- SQL Server는 다중컬럼 IN을 지원하지 않습니다. EXISTS나 조인으로 바꿔 씁니다.
연관 서브쿼리
자기 상품의 평균 주문수량보다 많이 주문한 줄을 찾습니다.
SELECT a.주문번호, a.상품번호, a.주문수량
FROM 주문상품 a
WHERE a.주문수량 > (SELECT AVG(b.주문수량)
FROM 주문상품 b
WHERE b.상품번호 = a.상품번호)
ORDER BY a.주문번호, a.상품번호;
| 주문번호 | 상품번호 | 주문수량 |
|---|---|---|
| 5001 | P10 | 2 |
| 5003 | P30 | 3 |
| 5004 | P20 | 2 |
| 5005 | P10 | 2 |
| 5006 | P50 | 2 |
- 상품별 평균 주문수량은 P10 1.67(2, 1, 2), P20 1.5(1, 2), P30 2(3, 1, 2), P50 1.5(1, 2), P60 1(1)입니다. 각 줄을 「자기 상품의 평균」과 비교하고, P60은 한 줄뿐이라 평균과 같아서 빠집니다.
- 메인쿼리의
a.상품번호가 바뀔 때마다 서브쿼리 값이 달라지므로, 비연관 서브쿼리처럼 「서브쿼리를 먼저 한 번 실행한 결과」로 바꿔 생각할 수 없습니다.
8. 뷰

뷰(View)는 SELECT 문에 이름을 붙여 저장해 두고 테이블처럼 쓰는 객체입니다. 1과목 「데이터 모델링 기초, 모델링 단계와 엔터티」의 쇼핑몰 ERD에서 주문에는 계산값인 총주문금액이 있었고, 「정규화, 트랜잭션, NULL 속성」에서는 이것을 반정규화의 예로 들었습니다. 2과목 예시 주문 테이블에는 이 컬럼을 두지 않았는데, 저장하지 않고 필요할 때 뷰로 계산하면 됩니다.
CREATE OR REPLACE VIEW 주문금액 AS
SELECT 주문번호, SUM(주문수량 * 주문단가) AS 총주문금액
FROM 주문상품
GROUP BY 주문번호;
SELECT 주문번호, 총주문금액
FROM 주문금액
WHERE 총주문금액 >= 10000; -- 5001 10000, 5004 12000, 5007 29000
DROP VIEW 주문금액;
| 특징 | 내용 |
|---|---|
| 데이터를 저장하지 않음 | 정의(SELECT 문)만 저장하고, 조회할 때마다 원본 테이블에서 계산 |
| 원본 변경이 바로 반영 | 주문 5002의 사과 수량을 1에서 3으로 바꾸면 뷰의 5002 금액도 3000에서 9000으로 바뀜 |
| 테이블처럼 조회·조인 | 뷰 + 주문 + 고객 조인 → 박세미 41000, 김하나 16000, 이두리 11000, 최네오 8000 |
| 집계 뷰는 수정 불가 | GROUP BY나 집계 함수가 든 뷰에는 INSERT 등을 할 수 없음(ORA-01733) |
| 삭제 | DROP VIEW 뷰이름. 원본 테이블은 그대로이고, 삭제한 뷰를 조회하면 오류(ORA-00942) |
| 정의 변경 | 오라클은 CREATE OR REPLACE VIEW로 다시 만듦. ALTER VIEW로는 SELECT 문을 바꿀 수 없음 |
수험서는 뷰의 장점을 세 가지로 정리합니다.
- 독립성: 테이블 구조가 바뀌어도 뷰를 쓰는 프로그램은 그대로 둘 수 있습니다.
- 편리성: 복잡한 질의를 뷰로 만들어 두면 짧게 조회할 수 있습니다.
- 보안성: 숨기고 싶은 컬럼을 뺀 뷰만 공개할 수 있습니다.
SQL Server는 ALTER VIEW로 뷰 정의를 바꿀 수 있습니다. 「ALTER VIEW로 정의를 바꿀 수 없다」는 보기는 오라클 기준으로 읽습니다.
9. SQL Server와의 차이

| 항목 | 오라클 | SQL Server |
|---|---|---|
| 다중컬럼 IN | 가능 | 지원 안 함(오류) |
| 인라인 뷰 별칭 | 생략 가능 | 반드시 필요 |
| 서브쿼리 안 ORDER BY | 인라인 뷰에서 가능 | TOP 등이 없으면 오류 |
| 스칼라 서브쿼리 2행 이상 | 오류(ORA-01427) | 오류 |
| NOT IN + NULL | 0행 | 0행(같음) |
| ANY, ALL, EXISTS | 같음 | 같음 |
인라인 뷰에 별칭을 붙이지 않은 SELECT ... FROM (SELECT ... GROUP BY 주문번호) WHERE 주문금액 >= 10000은 오라클에서는 실행되지만(5001, 5004, 5007) SQL Server에서는 문법 오류입니다.
10. 헷갈리기 쉬운 점

- NOT IN + NULL = 0행입니다. 서브쿼리 결과에 NULL이 하나만 있어도 NOT IN은 어떤 행도 돌려주지 않습니다. IN은 괜찮습니다. NOT EXISTS나
IS NOT NULL조건으로 피합니다. > ANY는 최솟값보다 큼,> ALL은 최댓값보다 큼입니다. 「ANY는 하나라도, ALL은 전부」로 풀어 읽으면 헷갈리지 않습니다.- 빈 서브쿼리: ALL은 참(전부 통과), ANY는 거짓(0행), 단일행 비교는 0행, 스칼라 서브쿼리는 NULL입니다.
- 단일행 연산자(
=,>)에는 1행만 옵니다. 판매가가 같은 상품이 둘이면 오류가 납니다. 여러 행이 올 수 있으면 IN이나 ANY를 씁니다. - ORDER BY는 WHERE 절 서브쿼리에 쓰지 않습니다. FROM 절 인라인 뷰에서는 쓸 수 있고, Top N 쿼리가 그 예입니다.
- 메인쿼리는 서브쿼리의 컬럼을 쓸 수 없습니다. 서브쿼리 결과를 메인쿼리에서 쓰려면 인라인 뷰로 옮기거나 조인합니다.
- IN과 EXISTS는 행을 늘리지 않지만 조인은 늘릴 수 있습니다. 주문된 상품을 IN으로 찾으면 5행, 조인하면 11행입니다.
- EXISTS의 SELECT 목록은 상관없습니다.
SELECT 1,SELECT *,SELECT NULL모두 같은 결과입니다. - 뷰는 데이터를 저장하지 않습니다. 원본이 바뀌면 바로 반영되고, 집계가 든 뷰에는 INSERT할 수 없습니다.
11. 연습 문제

서브쿼리 종류, ANY와 ALL, NOT IN과 NULL, 연관 서브쿼리, 뷰를 확인하는 문제입니다. 실제 시험과 같은 유형으로 새로 만들었습니다. 정답과 풀이는 다음 장에 있으니, 먼저 직접 풀어 보세요.
문제 1. [고객]과 [쿠폰] 테이블이 다음과 같을 때, SQL의 결과로 옳은 것은?
[고객]
| 고객번호 | 고객명 |
|---|---|
| C01 | 김하나 |
| C02 | 이두리 |
| C03 | 박세미 |
| C04 | 최네오 |
| C05 | 정다솜 |
[쿠폰]
| 쿠폰번호 | 고객번호 |
|---|---|
| K01 | C01 |
| K02 | C03 |
| K03 | NULL |
SELECT 고객번호 FROM 고객
WHERE 고객번호 NOT IN (SELECT 고객번호 FROM 쿠폰);
① C02, C04, C05
② C01, C03
③ C02, C04, C05, NULL
④ 결과 없음
문제 2. [상품] 테이블이 다음과 같을 때, 두 SQL의 결과 행 수를 차례대로 고른 것은?
| 상품번호 | 상품명 | 판매가 |
|---|---|---|
| P10 | 사과 | 3000 |
| P20 | 배 | 4000 |
| P30 | 감 | 2000 |
| P40 | 귤 | 5000 |
| P50 | 청사과 | 4000 |
| P60 | 배 선물세트 | 25000 |
(가) SELECT 상품번호 FROM 상품
WHERE 판매가 > ALL (SELECT 판매가 FROM 상품 WHERE 상품명 LIKE '%사과');
(나) SELECT 상품번호 FROM 상품
WHERE 판매가 > ANY (SELECT 판매가 FROM 상품 WHERE 상품명 LIKE '%사과');
① 2, 4
② 4, 2
③ 2, 5
④ 1, 4
문제 3. 고객 C01~C04는 주문이 1건 이상 있고(C01은 2건), C05는 주문이 없다. 오라클 기준으로 오류가 발생하는 SQL은?
① SELECT c.고객번호, (SELECT COUNT(*) FROM 주문 o WHERE o.고객번호 = c.고객번호) FROM 고객 c;
② SELECT c.고객번호, (SELECT MAX(o.주문번호) FROM 주문 o WHERE o.고객번호 = c.고객번호) FROM 고객 c;
③ SELECT c.고객번호, (SELECT o.주문번호 FROM 주문 o WHERE o.고객번호 = c.고객번호) FROM 고객 c;
④ SELECT o.주문번호, (SELECT c.고객명 FROM 고객 c WHERE c.고객번호 = o.고객번호) FROM 주문 o;
문제 4. [주문상품] 테이블이 다음과 같을 때, SQL의 결과에 포함되지 않는 행은?
| 주문번호 | 상품번호 | 주문수량 |
|---|---|---|
| 5001 | P10 | 2 |
| 5002 | P10 | 1 |
| 5005 | P10 | 2 |
| 5003 | P30 | 3 |
| 5005 | P30 | 1 |
| 5007 | P30 | 2 |
SELECT a.주문번호, a.상품번호
FROM 주문상품 a
WHERE a.주문수량 > (SELECT AVG(b.주문수량)
FROM 주문상품 b
WHERE b.상품번호 = a.상품번호);
① (5001, P10)
② (5002, P10)
③ (5005, P10)
④ (5003, P30)
문제 5. 서브쿼리와 뷰에 대한 설명으로 가장 적절하지 않은 것은?
① WHERE 절의 서브쿼리 안에서 ORDER BY로 정렬한 뒤 메인쿼리와 비교할 수 있다.
② 스칼라 서브쿼리가 0행을 돌려주면 그 값은 NULL이 된다.
③ 연관 서브쿼리는 메인쿼리의 컬럼을 서브쿼리 안에서 사용한다.
④ 뷰는 데이터를 따로 저장하지 않고 정의된 SELECT 문만 저장한다.
12. 풀이
문제 1. 정답 ④
서브쿼리 결과(C01, C03, NULL)에 NULL이 있어, 어떤 고객번호에 대해서도 <> NULL 비교가 UNKNOWN이 됩니다. NOT IN 전체가 TRUE가 될 수 없으므로 결과가 없습니다. ①을 얻으려면 서브쿼리에 WHERE 고객번호 IS NOT NULL을 넣거나 NOT EXISTS를 씁니다.
문제 2. 정답 ①
서브쿼리 결과는 사과 3000, 청사과 4000입니다. (가) > ALL은 가장 큰 값 4000보다 큰 P40(5000), P60(25000)의 2행이고, (나) > ANY는 가장 작은 값 3000보다 큰 P20, P40, P50, P60의 4행입니다.
문제 3. 정답 ③
주문이 2건인 고객(C01 등)에서 스칼라 서브쿼리가 2행을 돌려주어 오류(ORA-01427)입니다. ①과 ②는 집계 함수라 항상 1행이고(C05는 각각 0과 NULL), ④는 고객번호가 고객의 기본키라 1행입니다.
문제 4. 정답 ②
P10의 평균 수량은 (2 + 1 + 2) ÷ 3 ≈ 1.67이라 수량 1인 (5002, P10)은 평균보다 크지 않습니다. (5001, P10)과 (5005, P10)은 2라서 포함됩니다. P30의 평균은 2이므로 수량 3인 (5003, P30)은 포함되고, (5005, P30)과 (5007, P30)은 빠집니다.
문제 5. 정답 ①
WHERE 절 서브쿼리에는 ORDER BY를 쓰지 않습니다(오류 ORA-00907). ORDER BY는 FROM 절 인라인 뷰에서 Top N을 구할 때처럼 씁니다. ②, ③, ④는 맞는 설명입니다.
마치며
이번 글에서는 서브쿼리와 뷰를 정리했습니다.
- 서브쿼리는 위치(스칼라, 인라인 뷰, 중첩), 반환 형태(단일행, 다중행, 다중컬럼), 메인쿼리와의 관계(비연관, 연관)로 나눕니다.
- 스칼라 서브쿼리는 0행이면 NULL, 2행 이상이면 오류입니다. 단일행 연산자 뒤 서브쿼리도 2행 이상이면 오류입니다.
> ANY는 최솟값보다,> ALL은 최댓값보다 크다는 뜻입니다. 예시에서> ANY는 4행,> ALL은 2행입니다.- NOT IN 서브쿼리 결과에 NULL이 하나라도 있으면 결과는 0행입니다. 쿠폰 K03의 NULL 때문에 「쿠폰을 받지 않은 고객」이 0행이 되었고, NOT EXISTS로 고치면 C02, C04, C05입니다.
- ORDER BY는 WHERE 절 서브쿼리에는 쓰지 않고, FROM 절 인라인 뷰에서는 쓸 수 있습니다.
- 뷰는 데이터를 저장하지 않고 SELECT 문만 저장합니다. 장점은 독립성, 편리성, 보안성입니다.
다음 글 「집합 연산자와 ROLLUP·CUBE·GROUPING SETS」에서는 두 SELECT 결과를 합치고 빼는 UNION, UNION ALL, INTERSECT, MINUS와, 소계와 총계를 한 번에 구하는 ROLLUP, CUBE, GROUPING SETS, GROUPING 함수를 쇼핑몰 예시로 살펴보고 기출 유형 문제로 확인합니다.
참고 자료(공식)
- 데이터자격검정, SQL 개발자(SQLD) 안내: 출제 범위
- Oracle Database SQL Language Reference, Using Subqueries
- Oracle Database SQL Language Reference, Scalar Subquery Expressions
- Oracle Database SQL Language Reference, Comparison Conditions(ANY, SOME, ALL)
- Oracle Database SQL Language Reference, IN Condition
- Oracle Database SQL Language Reference, EXISTS Condition
- Oracle Database SQL Language Reference, CREATE VIEW
- Microsoft Learn, Subqueries (SQL Server)
'DB' 카테고리의 다른 글
| 윈도우 함수, 순위·집계·LAG/LEAD (0) | 2026.10.08 |
|---|---|
| 집합 연산자와 ROLLUP·CUBE·GROUPING SETS (0) | 2026.10.08 |
| 표준 조인, INNER·OUTER·CROSS JOIN (0) | 2026.10.08 |
| 조인 기초, 등가 조인과 오라클 (+) 표기 (0) | 2026.10.08 |
| GROUP BY와 HAVING, 집계 함수와 NULL (0) | 2026.10.08 |