SQL을 처음 배울 때 서브쿼리로 모든 걸 해결하려다 쿼리가 점점 깊어지는 경험을 하게 됩니다. 중첩이 3단계를 넘어가면 본인이 쓴 쿼리도 다시 읽기가 힘들어지거든요. CTE를 알고 나서 쿼리를 구조화하는 방식이 바뀌었습니다.
오늘은 서브쿼리와 CTE가 어떻게 다른지, 각각 언제 쓰는 게 적합한지 정리해보겠습니다.
1. 서브쿼리
서브쿼리는 쿼리 안에 중첩된 쿼리입니다. SELECT, FROM, WHERE 절 어디에나 들어갈 수 있고, 외부 쿼리의 결과를 필터링하거나 가공하는 데 씁니다. 간단한 필터링이나 집계 결과를 한 번만 참조할 때는 서브쿼리가 빠르고 직관적입니다.
서브쿼리가 위치하는 곳에 따라 성격이 달라집니다. WHERE 절 서브쿼리는 조건 필터링에, FROM 절 서브쿼리(인라인 뷰)는 임시 테이블처럼 활용합니다. SELECT 절 서브쿼리는 각 행마다 값을 계산할 때 씁니다.
1.1 상관 서브쿼리
외부 쿼리의 각 행을 참조하는 서브쿼리입니다. 외부 쿼리 행 수만큼 반복 실행되기 때문에 데이터가 많을수록 성능이 급격히 나빠집니다. 상관 서브쿼리가 필요한 상황이라면 JOIN이나 윈도우 함수로 대체할 수 있는지 먼저 검토하는 게 좋습니다.
-- WHERE 절 서브쿼리: 평균 이상 주문 금액 조회
SELECT
order_id,
customer_id,
amount
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
-- FROM 절 서브쿼리 (인라인 뷰): 고객별 총 주문 집계 후 필터
SELECT *
FROM (
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
) AS customer_summary
WHERE order_count >= 3;
2. CTE (Common Table Expression)
CTE는 WITH 절로 시작하는 임시 결과 집합입니다. 쿼리 실행 전에 이름을 붙인 임시 테이블을 정의하고, 이후 메인 쿼리에서 그 이름으로 참조합니다. 서브쿼리와 논리적으로 같은 결과를 내지만, 쿼리의 구조를 위에서 아래로 읽을 수 있게 만들어줍니다.
가장 큰 장점은 가독성입니다. 복잡한 쿼리를 단계별로 분리해서 각 단계에 이름을 붙이면, 쿼리가 무엇을 하는지 흐름이 명확해집니다. 같은 집계 결과를 여러 번 참조해야 할 때도 CTE로 한 번 정의해두면 중복 없이 재사용할 수 있습니다.
2.1 다중 CTE
CTE는 여러 개를 쉼표로 연결해서 순서대로 정의할 수 있습니다. 앞에서 정의한 CTE를 뒤에서 참조하는 것도 가능합니다. 복잡한 분석 쿼리를 단계별로 쌓아 올리는 방식으로 쓰면, 각 단계를 독립적으로 확인하고 디버깅할 수 있습니다.
-- 단일 CTE: 고객별 주문 요약
WITH customer_summary AS (
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
)
SELECT *
FROM customer_summary
WHERE order_count >= 3
ORDER BY total_amount DESC;
-- 다중 CTE: 단계별로 쌓아 올리기
WITH
monthly_sales AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(amount) AS sales
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
),
sales_with_growth AS (
SELECT
month,
sales,
LAG(sales) OVER (ORDER BY month) AS prev_sales,
ROUND(
(sales - LAG(sales) OVER (ORDER BY month))
/ LAG(sales) OVER (ORDER BY month) * 100, 1
) AS growth_rate
FROM monthly_sales
)
SELECT *
FROM sales_with_growth
WHERE growth_rate IS NOT NULL
ORDER BY month;
3. 재귀 CTE
CTE의 또 다른 강점은 재귀 쿼리입니다. 조직도처럼 계층 구조를 가진 데이터를 탐색할 때 씁니다. 일반 서브쿼리나 JOIN으로는 계층 깊이가 몇 단계인지 모르는 구조를 탐색할 수 없습니다. 재귀 CTE는 종료 조건이 충족될 때까지 자기 자신을 반복 참조하면서 계층을 타고 내려갑니다.
ANCHOR 쿼리(초기값)와 RECURSIVE 쿼리(반복 실행)로 구성됩니다. 예를 들어 직원 테이블에서 특정 관리자 아래의 모든 직원을 뽑을 때, 관리자 ID를 anchor로 잡고 해당 ID를 manager_id로 가진 직원들을 반복적으로 조회합니다.
-- 재귀 CTE: 조직도 계층 탐색
WITH RECURSIVE org_hierarchy AS (
-- ANCHOR: 최상위 관리자
SELECT
employee_id,
name,
manager_id,
1 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- RECURSIVE: 하위 직원 반복 탐색
SELECT
e.employee_id,
e.name,
e.manager_id,
h.depth + 1
FROM employees e
JOIN org_hierarchy h ON e.manager_id = h.employee_id
)
SELECT
CONCAT(REPEAT(' ', depth - 1), name) AS org_chart,
depth
FROM org_hierarchy
ORDER BY depth, employee_id;
4. 서브쿼리 vs CTE 선택 기준
성능 면에서는 대부분의 DBMS에서 CTE와 서브쿼리가 실행 계획이 동일하게 최적화됩니다. 단순히 CTE가 더 빠르거나 서브쿼리가 더 빠른 게 아니라, 실행 계획은 옵티마이저가 결정합니다. 성능 차이가 나는 경우는 CTE를 Materialized(물리적으로 임시 저장)로 처리하느냐, Non-materialized로 처리하느냐에 따라 달라지는데, 이는 DBMS마다 다르게 동작합니다.
결국 선택 기준은 가독성과 재사용성입니다. 한 번만 참조하는 단순한 조건 필터링이라면 서브쿼리가 더 간결합니다. 같은 집계를 여러 번 참조하거나, 쿼리 로직을 단계별로 명확히 보여줘야 한다면 CTE가 낫습니다.
| 항목 | 서브쿼리 | CTE |
|---|---|---|
| 가독성 | 중첩 깊어지면 떨어짐 | 위에서 아래로 읽기 쉬움 |
| 재사용 | 같은 쿼리 반복 작성 필요 | 한 번 정의 후 여러 번 참조 |
| 디버깅 | 중간 결과 확인 어려움 | 각 CTE 단독 실행 가능 |
| 계층 구조 | 불가 | 재귀 CTE로 처리 가능 |
| 적합한 상황 | 단순 필터링, 단일 참조 | 복잡한 로직, 다단계 집계 |
정리
서브쿼리로 시작해서 CTE로 리팩토링하는 과정이 쿼리 작성 실력이 늘어가는 과정과 비슷한 것 같습니다. 처음엔 동작만 하면 됐는데, 나중에 같은 쿼리를 다시 봤을 때 이해가 안 되는 경험을 하고 나서 구조를 의식하게 됩니다.
CTE로 단계를 명시적으로 나누는 습관이 생기면, 복잡한 분석 쿼리도 다른 사람이 읽기 쉬운 형태로 정리할 수 있습니다. 😊
'Language > SQL' 카테고리의 다른 글
| [SQL] 윈도우 함수 (Windw Function) (0) | 2026.06.05 |
|---|---|
| [HackerRank] Contest Leaderboard (MySQL) (0) | 2026.03.16 |
| [HackerRank] Weather Observation Station 5 (MySQL) (0) | 2026.02.23 |
| [HackerRank] Average Population of Each Continent (MySQL) (0) | 2026.02.23 |
| [HackerRank] Contest Leaderboard (MySQL) (0) | 2026.02.13 |
HELLO WORLD
안녕하세요. 데이터로 말하는 분석가 모모입니다.
데이터를 구조화하고 분석하는 과정과 실무에 활용되는 도구 중심의 내용을 기록합니다.