본문 바로가기

서브쿼리 종류와 NOT IN의 NULL 함정

DB SorryP 2026. 10. 8. 18:23
반응형

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. 서브쿼리의 종류와 공통 규칙

3층 건물 단면도. 3층 SELECT 층에는 스칼라 서브쿼리, 2층 FROM 층에는 인라인 뷰, 1층 WHERE 층에는 중첩 서브쿼리. WHERE 층은 ORDER BY 출입 금지(ORA-00907), FROM 층은 ORDER BY 환영과 Top N 표지이고 인라인 뷰 정렬 결과는 P60 25000, P40 5000. 선배가 엘리베이터를 맡고 초보가 층별 안내도를 봅니다

서브쿼리(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. 스칼라 서브쿼리

버튼 하나에 값 하나만 나오는 자판기. C05 쿠폰명은 빈손 NULL, C05 주문건수는 0, 초보가 누른 C01 주문번호는 5001과 5003 두 캔이 나오려다 ORA-01427. 로봇이 한 번에 한 캔 스티커를 붙이고, 아래에 주문건수 2, 2, 2, 1, 0과 쿠폰명 표

스칼라 서브쿼리는 값 하나(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. 인라인 뷰

모자 친구가 주문별 금액 임시 표 5001 10000, 5002 3000, 5003 6000, 5004 12000, 5005 8000, 5006 8000, 5007 29000을 작업대에 펼치고, 선배가 10000 이상만 집게로 집어 5001, 5004, 5007을 접시에 담습니다

인라인 뷰는 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. 단일행·다중행 서브쿼리

판매가 눈금자에 사과 3000, 청사과 4000 깃발. ANY보다 큼 팀은 3000만 넘으면 되어 P20, P40, P50, P60 4행, ALL보다 큼 팀은 4000까지 넘어야 해서 P40, P60 2행. 단일행 회전문에는 사과와 청사과가 함께 끼어 ORA-01427. 초보가 ANY는 제일 작은 깃발만 넘으면 OK라고 외칩니다

단일행 서브쿼리

결과가 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

로봇이 고객 다섯 명의 문을 두드립니다. 주문이 있는 C01부터 C04까지는 등이 켜지고 C05만 꺼져 있습니다. SELECT 1, SELECT 별표, SELECT NULL 무엇이든 4명으로 같고, 선배가 NOT EXISTS 목록에 C05 정다솜을 적습니다

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 때문에 C02, C04, C05 누구도 지나가지 못해 0행이고 모자 친구가 0행 표지판 옆에 있습니다. 선배가 IS NOT NULL 지우개로 물음표를 지우면 NOT EXISTS처럼 C02, C04, C05가 나옵니다

「쿠폰을 받지 않은 고객」을 구해 보겠습니다. 쿠폰 테이블의 고객번호는 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. 다중컬럼 서브쿼리와 연관 서브쿼리

고객번호와 주문일시를 한 고리에 꿴 열쇠 꾸러미와 결과 5003, 5005, 5006, 5007. 컨베이어 벨트의 주문상품 줄마다 자기 상품 평균 P10 1.67, P20 1.5, P30 2, P50 1.5, P60 1과 비교해 5001-P10, 5003-P30, 5004-P20, 5005-P10, 5006-P50만 통과합니다

다중컬럼 서브쿼리

고객별 가장 최근 주문을 구합니다.

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. 뷰

주문상품 창고 벽의 주문금액 창문은 유리뿐이라 데이터를 저장하지 않습니다. 초보가 5002 사과 수량을 1에서 3으로 바꾸자 창문의 금액이 3000에서 9000으로 바뀝니다. 명판에 독립성, 편리성, 보안성, 선배는 ERD의 총주문금액은 저장 대신 창문으로라고 말합니다. 집계 뷰 INSERT는 ORA-01733

뷰(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와의 차이

두 주방 비교. 오라클 주방은 고객번호와 주문일시 쌍둥이 접시인 다중컬럼 IN을 받고 이름표 없는 인라인 뷰 냄비도 씁니다. SQL Server 주방은 다중컬럼 IN이 오류이고 인라인 뷰에 별칭이 반드시 필요합니다. 가운데 NOT IN과 NULL 냄비는 둘 다 0행

항목 오라클 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행, ANY는 최솟값 ALL은 최댓값, 빈 서브쿼리 ALL은 참, WHERE 서브쿼리에 ORDER BY 불가, EXISTS 목록은 상관없음. 초보와 모자 친구가 낚싯대를 드리우고 선배 옆에 바구니가 있습니다

  • 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. 연습 문제

칠판에 문제 5개, 풀이는 아래에와 문제 주제 다섯 가지. 초보, 모자 친구, 로봇 앞에 상자 안에 상자가 든 선물 상자가 있고, 선배가 다음은 집합 연산자 쪽지를 붙입니다

서브쿼리 종류, 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 함수를 쇼핑몰 예시로 살펴보고 기출 유형 문제로 확인합니다.

참고 자료(공식)

반응형