서브쿼리란 WHERE 서브쿼리 IN + 서브쿼리 EXISTS FROM 서브쿼리 📝 문제풀이
◀ 이전 📋 목차 CH 08 ▶
🎁 CHAPTER 07 · 중급

쿼리 속의 쿼리, 서브쿼리

"평균보다 나이가 많은 사람"처럼, 결과를 먼저 구해야 다음 질문에 답할 수 있는 경우가 있어요. 이럴 때 쿼리 안에 또 다른 쿼리를 넣는 서브쿼리가 등장합니다. 안쪽부터 하나씩 풀어볼게요.

🎯 이 장을 끝내면
📖
서브쿼리란
쿼리 안에 들어간 또 하나의 쿼리
괄호로 감싸서 넣고, 안쪽부터 실행돼요.

서브쿼리(subquery)다른 SQL 문 안에 들어간 또 하나의 SELECT이에요. 항상 괄호 ( )로 감싸요. 핵심 원리는 하나입니다 — "먼저 안쪽(서브쿼리)을 구하고, 그 결과를 바깥쪽에서 사용한다."

1️⃣
안쪽 (서브쿼리)
괄호 안을 먼저 실행해 값·목록을 구해요
2️⃣
바깥 (메인 쿼리)
그 결과를 조건에 넣어 최종 실행
🧩 왜 필요할까? — "평균보다 많은 사람" "나이가 평균보다 많은 사람을 뽑아줘"라는 요청을 생각해봐요.
문제는 평균 나이가 얼마인지 미리 알 수 없다는 거예요. 데이터가 바뀌면 평균도 바뀌니까요.
그래서 ① 먼저 평균 나이를 계산하고 → ② 그 값보다 큰 사람을 조회해야 해요. 이 두 단계를 한 문장으로 합친 게 서브쿼리예요. 💡 즉 "한 번의 조회로는 답이 안 나오고, 먼저 다른 조회 결과가 필요할 때" 서브쿼리를 씁니다.
users 테이블 (예시)
idnameage
1'철수'20
2'영희'25
3'민수'30
서브쿼리 쿼리 속의 쿼리 스칼라 IN EXISTS 상관 서브쿼리 파생 테이블 AVG
🧭
읽는 순서 팁. 서브쿼리가 든 문장을 만나면, 무조건 괄호 안쪽부터 먼저 눈으로 읽으세요. "안쪽이 어떤 값(또는 목록)을 만들지"를 그려야, 바깥 문장의 의미가 보여요.
🔎
WHERE 서브쿼리
스칼라 서브쿼리 + 비교 연산자
단일 값을 돌려주는 서브쿼리를 조건에 바로 씁니다.

가장 흔한 형태는 WHERE 절 안에 서브쿼리를 넣는 거예요. 앞의 "평균보다 많은 사람"을 SQL로 쓰면 이렇게 돼요.

SELECT *
FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- ① 안쪽: 평균 나이를 구함 → 25
-- ② 바깥: age > 25 인 행만 조회 → '민수'(30)
🔤
AVG(age) — 나이의 평균을 구하는 집계 함수예요(average). 결과가 숫자 하나로 딱 나와서, 바로 아래 "스칼라 서브쿼리"의 좋은 예가 돼요.
📏 스칼라 서브쿼리 (scalar subquery) (SELECT AVG(age) FROM users)처럼 결과가 딱 하나의 값(행 1개 · 열 1개)인 서브쿼리를 스칼라 서브쿼리라고 해요.
값이 하나뿐이라, = · > · < · >= 같은 비교 연산자와 곧바로 쓸 수 있어요. ⚠️ 만약 서브쿼리가 값을 여러 개 돌려주면 age > (...) 같은 비교는 에러가 나요. 이럴 땐 다음 절의 IN을 써야 해요.
💡
위 쿼리에서 평균을 미리 25로 계산해 WHERE age > 25라고 써도 결과는 같아요. 하지만 데이터가 바뀌면 다시 계산해야 하죠. 서브쿼리를 쓰면 DB가 항상 최신 평균으로 자동 계산해줘서 안전해요.
📋
IN + 서브쿼리
서브쿼리가 값 목록을 돌려줄 때
여러 값 중 하나에 해당하는지 검사해요.

서브쿼리가 여러 행(값 목록)을 돌려줄 때는 IN을 써요. "이 목록 안에 있으면"이라는 뜻이죠. 예를 들어 "주문을 한 번이라도 한 사용자"를 뽑아봐요.

SELECT *
FROM users
WHERE id IN (SELECT user_id FROM orders);
-- 안쪽: orders 에 있는 user_id 들의 목록 → (1, 3, 3, ...)
-- 바깥: id 가 그 목록에 포함된 사용자만 조회
상황서브쿼리 결과사용 방법
단일 값값 1개 (예: 25)=, >, <비교 연산자
값 목록여러 값 (예: 1, 3, 5)IN / NOT IN
🔤
단일행 · 다중행 연산자 — = > <처럼 값 하나하고만 비교하는 걸 단일행 연산자, IN처럼 여러 값과 비교하는 걸 다중행 연산자라고 해요. 서브쿼리가 여러 행을 돌려주면 반드시 다중행 연산자를 써야 해요.
🔎 IN의 친척 — ANY · ALL IN 말고도 목록과 비교하는 연산자가 있어요.
ANY — 목록 중 하나라도 조건을 만족하면 참. 예: > ANY (...)최솟값보다 크면 참이고, = ANY (...)IN과 같아요.
ALL — 목록의 모든 값에 대해 조건을 만족해야 참. 예: > ALL (...)최댓값보다 커야 참이에요. 💡 헷갈리면: ANY는 "하나라도"(최솟값 기준), ALL은 "전부"(최댓값 기준)로 기억하세요.
🔁
NOT IN 도 있어요. "주문을 한 적 없는 사용자"는 WHERE id NOT IN (SELECT user_id FROM orders)로 뒤집으면 돼요. 목록에 포함되지 않은 행만 남아요.
EXISTS
"값"이 아니라 "존재 여부"만 확인
있으면 참, 없으면 거짓 — 상관 서브쿼리예요.

EXISTS는 서브쿼리 결과가 한 행이라도 있으면 참(true), 하나도 없으면 거짓(false)이에요. 무엇이 나오는지는 관심 없고, "있느냐 없느냐"만 봐요. 그래서 안에 SELECT 1처럼 아무 값이나 넣어도 됩니다.

SELECT *
FROM users
WHERE EXISTS (
  SELECT 1 FROM orders
  WHERE orders.user_id = users.id
);
-- 바깥의 각 users 행마다, 그 사람의 주문이 orders 에 하나라도 있으면 통과
🔗 상관 서브쿼리 (correlated subquery) 위 서브쿼리 안의 orders.user_id = users.id를 보세요. 안쪽 쿼리가 바깥쪽 테이블(users)의 값을 참조하고 있어요.
이렇게 바깥 행에 의존하는 서브쿼리상관 서브쿼리라고 해요. 바깥의 행마다 한 번씩 안쪽이 다시 실행되는 셈이에요. 💡 반대로, 바깥과 무관하게 혼자 실행되는(sec-1·sec-2 예시) 건 "비상관 서브쿼리"예요.
INEXISTS
확인하는 것값이 목록에 포함되는지조건에 맞는 행이 존재하는지
서브쿼리 결과값 목록을 실제로 만듦존재 여부만(값은 무시)
자주 쓰는 곳목록이 작고 명확할 때존재 확인·상관 조건일 때
🧭
많은 경우 INEXISTS같은 결과를 냅니다. "목록으로 비교할까(IN)", "존재만 확인할까(EXISTS)"라는 표현 방식의 차이로 먼저 이해하세요.
🧱
FROM 서브쿼리
FROM 절 서브쿼리 = 파생 테이블
서브쿼리 결과를 하나의 임시 테이블처럼 써요.

서브쿼리는 WHERE뿐 아니라 FROM에도 올 수 있어요. 이때 서브쿼리의 결과는 이름 붙은 임시 테이블처럼 취급되는데, 이를 파생 테이블(derived table)이라고 불러요.

SELECT t.name, t.age
FROM (SELECT name, age FROM users WHERE age >= 20) AS t
WHERE t.age < 30;
-- 안쪽 결과를 t 라는 임시 테이블로 만들고, 바깥에서 다시 조회
💡
별칭(AS t)은 거의 필수. FROM 절 서브쿼리에는 대부분 이름(별칭)을 붙여줘야 바깥에서 t.age처럼 참조할 수 있어요. 지금은 "이런 것도 되는구나" 정도만 잡아두면 충분해요.
⚠️
서브쿼리 남용 주의. 서브쿼리는 강력하지만, 깊게 겹치면 읽기 어렵고 느려질 수 있어요. 같은 일을 JOIN(CH 06)으로 더 간단히 할 수 있는 경우도 많으니, 둘을 상황에 맞게 골라 쓰세요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 쿼리결과)을 섞어 두었어요.
CH 08 ✏️ 데이터 조작 — INSERT·UPDATE·DELETE로 데이터를 넣고·바꾸고·지우기