SQL에서 “서브쿼리를 써야 하나?”는 초보~중급 개발자가 가장 헷갈리는 포인트. 아래 패턴별 가이드를 참고하면 훨씬 쉽게 판단할 수 있다.
집계 결과를 기반으로 원본에서 조건에 맞는 행만 가져오고 싶을 때.
-- 예: 공간을 2개 이상 등록한 host의 모든 공간
SELECT p.*
FROM places p
JOIN (
SELECT host_id
FROM places
GROUP BY host_id
HAVING COUNT(*) >= 2
) heavy ON p.host_id = heavy.host_id;
“카테고리별 favorites 최댓값인 행만” 같은 문제.
SELECT r.*
FROM rest_info r
JOIN (
SELECT food_type, MAX(favorites) AS max_fav
FROM rest_info
GROUP BY food_type
) m ON r.food_type = m.food_type
AND r.favorites = m.max_fav;
해당 데이터가 있는지/없는지만 확인.
-- 주문 기록이 있는 유저
SELECT u.*
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
);
서브쿼리 결과를 컬럼으로 활용.
SELECT r.*,
(SELECT AVG(amount)
FROM orders o
WHERE o.r_id = r.id) AS avg_amount
FROM restaurant r;
복잡한 로직을 논리 단위로 나눔 → 가독성/디버깅 용이.
WITH base AS (...),
cleaned AS (SELECT ... FROM base WHERE ...),
scored AS (
SELECT ..., DENSE_RANK() OVER (...) AS rnk
FROM cleaned
)
SELECT * FROM scored WHERE rnk = 1;
해당 테이블에 매칭 데이터가 없는 행만 찾기.
SELECT r.*
FROM restaurant r
WHERE NOT EXISTS (
SELECT 1
FROM reviews v
WHERE v.r_id = r.id
);
같은 테이블 간 비교 전 필터링.
WITH first_last AS (
SELECT user_id,
MIN(order_date) AS first_dt,
MAX(order_date) AS last_dt
FROM orders
GROUP BY user_id
)
SELECT o1.user_id, o1.amount AS first_amt, o2.amount AS last_amt
FROM first_last fl
JOIN orders o1 ON o1.user_id=fl.user_id AND o1.order_date=fl.first_dt
JOIN orders o2 ON o2.user_id=fl.user_id AND o2.order_date=fl.last_dt;
그룹 내 통계/순위를 계산하고 각 행에 붙인 뒤 필터.
SELECT *
FROM (
SELECT p.*,
COUNT(*) OVER (PARTITION BY host_id) AS place_cnt
FROM places p
) t
WHERE place_cnt >= 2;
PARTITION BY로 그룹 내 집계 → 외부에서 필터.
존재성만? → EXISTS / NOT EXISTS
집계 조건 필터? → 파생테이블 조인
그룹 내 1등/최신 행? → MAX 조인 or 순위 함수
그룹 내 통계 컬럼화? → 윈도우 함수
단일 값 붙이기? → 스칼라 서브쿼리 or 조인
복잡해서 읽기 어려움? → CTE
매칭 없는 행? → NOT EXISTS