"평균보다 나이가 많은 사람"처럼, 결과를 먼저 구해야 다음 질문에 답할 수 있는 경우가 있어요. 이럴 때 쿼리 안에 또 다른 쿼리를 넣는 서브쿼리가 등장합니다. 안쪽부터 하나씩 풀어볼게요.
서브쿼리(subquery)는 다른 SQL 문 안에 들어간 또 하나의 SELECT 문이에요. 항상 괄호 ( )로 감싸요. 핵심 원리는 하나입니다 — "먼저 안쪽(서브쿼리)을 구하고, 그 결과를 바깥쪽에서 사용한다."
| id | name | age |
|---|---|---|
| 1 | '철수' | 20 |
| 2 | '영희' | 25 |
| 3 | '민수' | 30 |
가장 흔한 형태는 WHERE 절 안에 서브쿼리를 넣는 거예요. 앞의 "평균보다 많은 사람"을 SQL로 쓰면 이렇게 돼요.
SELECT *
FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- ① 안쪽: 평균 나이를 구함 → 25
-- ② 바깥: age > 25 인 행만 조회 → '민수'(30)
(SELECT AVG(age) FROM users)처럼 결과가 딱 하나의 값(행 1개 · 열 1개)인 서브쿼리를 스칼라 서브쿼리라고 해요.= · > · < · >= 같은 비교 연산자와 곧바로 쓸 수 있어요.
⚠️ 만약 서브쿼리가 값을 여러 개 돌려주면 age > (...) 같은 비교는 에러가 나요. 이럴 땐 다음 절의 IN을 써야 해요.
25로 계산해 WHERE age > 25라고 써도 결과는 같아요. 하지만 데이터가 바뀌면 다시 계산해야 하죠. 서브쿼리를 쓰면 DB가 항상 최신 평균으로 자동 계산해줘서 안전해요.서브쿼리가 여러 행(값 목록)을 돌려줄 때는 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 — 목록 중 하나라도 조건을 만족하면 참. 예: > ANY (...)는 최솟값보다 크면 참이고, = ANY (...)는 IN과 같아요.ALL — 목록의 모든 값에 대해 조건을 만족해야 참. 예: > ALL (...)은 최댓값보다 커야 참이에요.
💡 헷갈리면: ANY는 "하나라도"(최솟값 기준), ALL은 "전부"(최댓값 기준)로 기억하세요.
WHERE id NOT IN (SELECT user_id FROM orders)로 뒤집으면 돼요. 목록에 포함되지 않은 행만 남아요.EXISTS는 서브쿼리 결과가 한 행이라도 있으면 참(true), 하나도 없으면 거짓(false)이에요. 무엇이 나오는지는 관심 없고, "있느냐 없느냐"만 봐요. 그래서 안에 SELECT 1처럼 아무 값이나 넣어도 됩니다.
SELECT *
FROM users
WHERE EXISTS (
SELECT 1 FROM orders
WHERE orders.user_id = users.id
);
-- 바깥의 각 users 행마다, 그 사람의 주문이 orders 에 하나라도 있으면 통과
orders.user_id = users.id를 보세요. 안쪽 쿼리가 바깥쪽 테이블(users)의 값을 참조하고 있어요.IN | EXISTS | |
|---|---|---|
| 확인하는 것 | 값이 목록에 포함되는지 | 조건에 맞는 행이 존재하는지 |
| 서브쿼리 결과 | 값 목록을 실제로 만듦 | 존재 여부만(값은 무시) |
| 자주 쓰는 곳 | 목록이 작고 명확할 때 | 존재 확인·상관 조건일 때 |
IN과 EXISTS는 같은 결과를 냅니다. "목록으로 비교할까(IN)", "존재만 확인할까(EXISTS)"라는 표현 방식의 차이로 먼저 이해하세요.서브쿼리는 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처럼 참조할 수 있어요. 지금은 "이런 것도 되는구나" 정도만 잡아두면 충분해요.> = 등 비교 연산자와 함께 써요 (예: age > (SELECT AVG(age) FROM users)).IN으로 포함 여부를 검사해요.EXISTS는 값이 아니라 존재 여부(있으면 참)만 확인하며, 보통 상관 서브쿼리와 함께 써요.FROM 절 서브쿼리는 파생 테이블 — 결과를 임시 테이블처럼 다뤄요.