데이터 분석가를 위한 SQL: 윈도우 함수, CTE, 고급 쿼리 기법
SQL 윈도우 함수, CTE(공통 테이블 식), 고급 분석 쿼리를 실용적인 코드 예제와 함께 설명합니다. 데이터 분석가 면접 준비와 실무에 필수적인 기법입니다.

SQL 윈도우 함수, CTE(공통 테이블 식), 고급 쿼리 패턴은 분석 SQL의 핵심 기반을 구성합니다. 데이터 분석가 면접을 준비하든, 복잡한 리포팅 쿼리를 작성하든, 이러한 기법을 숙달하면 장황하고 읽기 어려운 서브쿼리를 간결하고 성능이 우수한 SQL로 변환할 수 있습니다.
윈도우 함수는 현재 행과 관련된 행 집합에 대해 계산을 수행합니다. GROUP BY처럼 결과를 단일 행으로 축소하지 않고, 각 행을 유지한 채 계산 결과를 추가하는 것이 특징입니다. CTE와 결합하면 복잡한 분석 쿼리도 읽기 쉽고 유지보수하기 쉬운 형태로 구성할 수 있습니다.
SQL 윈도우 함수와 OVER 절의 기본 이해
윈도우 함수는 정의된 "윈도우"(행의 범위)에 대해 계산을 적용합니다. OVER 절은 해당 윈도우에 포함되는 행과 정렬 순서를 제어합니다. GROUP BY를 사용하는 집계 함수와 달리, 윈도우 함수는 결과 집합의 모든 개별 행을 유지합니다.
아래의 구문은 PostgreSQL, MySQL 8 이상, BigQuery, SQL Server, Snowflake 등 주요 데이터베이스에서 공통적으로 사용됩니다.
-- window_function_syntax.sql
SELECT
employee_id,
department,
salary,
-- Ranks employees within each department by salary
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS dept_salary_rank,
-- Running total of salaries within department
SUM(salary) OVER (
PARTITION BY department
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_salary_total
FROM employees;PARTITION BY는 데이터를 그룹으로 분할합니다(GROUP BY와 유사하지만 행을 축소하지 않습니다). ORDER BY는 각 파티션 내의 순서를 정의합니다. 선택적 프레임 절(ROWS BETWEEN ...)은 윈도우를 특정 행 범위로 제한합니다.
ROW_NUMBER, RANK, DENSE_RANK 비교
이 세 가지 순위 함수는 외관상 유사하지만, 동일 순위(타이)가 존재할 때 동작 방식이 다릅니다. 잘못된 함수를 선택하는 것은 분석 쿼리에서 자주 발생하는 버그의 원인입니다.
-- ranking_comparison.sql
SELECT
product_name,
category,
revenue,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num,
RANK() OVER (ORDER BY revenue DESC) AS rank_val,
DENSE_RANK() OVER (ORDER BY revenue DESC) AS dense_rank_val
FROM products;| product_name | revenue | row_num | rank_val | dense_rank_val | |-------------|---------|---------|----------|----------------| | Widget A | 50000 | 1 | 1 | 1 | | Widget B | 50000 | 2 | 1 | 1 | | Widget C | 40000 | 3 | 3 | 2 | | Widget D | 30000 | 4 | 4 | 3 |
ROW_NUMBER는 항상 고유한 순차 정수를 할당합니다. 동일한 값의 행이 있어도 하나의 행에 임의로 더 작은 번호가 부여됩니다. RANK는 동일 순위 행에 같은 번호를 할당하지만 후속 번호를 건너뜁니다(1, 1, 3). DENSE_RANK도 동일 순위를 처리하지만 번호를 건너뛰지 않습니다(1, 1, 2). 상위 N개 쿼리에서 중복 값이 같은 순위를 공유해야 하는 경우, DENSE_RANK가 적절한 선택입니다.
LAG와 LEAD를 활용한 기간 대비 분석
LAG와 LEAD는 셀프 조인 없이 이전 또는 이후 행의 데이터에 접근할 수 있게 해줍니다. 전기 대비 성장률 계산, 트렌드 감지, 변화 식별에 필수적인 함수입니다.
-- period_over_period.sql
SELECT
month,
revenue,
-- Previous month revenue
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
-- Month-over-month growth rate
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0
/ NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0),
2
) AS mom_growth_pct,
-- Next month revenue (forward-looking)
LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_sales
ORDER BY month;LAG/LEAD의 두 번째 인수는 오프셋(기본값 1)을 지정합니다. 세 번째 선택적 인수는 해당 오프셋에 행이 존재하지 않을 때의 기본값을 제공하며, 첫 번째/마지막 행에서 NULL을 방지하는 데 유용합니다. 성장률 계산의 NULLIF는 0으로 나누기 오류를 방지합니다.
NTILE을 활용한 백분위 분할
NTILE은 지정된 수의 거의 동일한 그룹으로 행을 분배합니다. 데이터 분석가는 사분위 분석, 십분위 스코어링, 고객 세분화에 이 함수를 활용합니다.
-- customer_segmentation.sql
SELECT
customer_id,
total_spend,
-- Split customers into 4 quartiles by spend
NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile,
-- Decile scoring for finer granularity
NTILE(10) OVER (ORDER BY total_spend DESC) AS spend_decile
FROM (
SELECT
customer_id,
SUM(order_total) AS total_spend
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY customer_id
) customer_totals;사분위 1에는 지출 금액이 가장 높은 고객이, 사분위 4에는 가장 낮은 고객이 포함됩니다. 이 패턴은 마케팅 분석에서 사용되는 RFM(최근성, 빈도, 금액) 세분화에 직접 대응됩니다.
Data Analytics 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
공통 테이블 식(CTE): 중첩 서브쿼리의 대체
CTE(WITH 절)는 복잡한 쿼리를 이름이 지정된 읽기 쉬운 단계로 분해합니다. 각 CTE는 해당 쿼리의 실행 기간 동안에만 존재하는 임시 명명 결과 집합으로 기능합니다. 가독성 향상 외에도, 각 단계를 독립적으로 테스트할 수 있어 디버깅이 용이해집니다.
-- cte_sales_analysis.sql
WITH monthly_revenue AS (
-- Step 1: Aggregate raw orders into monthly totals
SELECT
DATE_TRUNC('month', order_date) AS month,
product_category,
SUM(amount) AS revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY DATE_TRUNC('month', order_date), product_category
),
ranked_categories AS (
-- Step 2: Rank categories within each month
SELECT
month,
product_category,
revenue,
unique_customers,
RANK() OVER (
PARTITION BY month
ORDER BY revenue DESC
) AS category_rank
FROM monthly_revenue
)
-- Step 3: Final output — top 3 categories per month
SELECT
month,
product_category,
revenue,
unique_customers,
category_rank
FROM ranked_categories
WHERE category_rank <= 3
ORDER BY month, category_rank;이 3단계 접근 방식은 깊이 중첩된 서브쿼리를 대체합니다. 각 CTE는 집계, 순위 매기기, 필터링이라는 명확한 책임을 가지고 있습니다.
재귀 CTE를 활용한 계층 데이터 처리
재귀 CTE는 계층 구조 또는 그래프형 데이터와 관련된 문제를 해결합니다. 조직도, 카테고리 트리, BOM(자재명세서), 경로 탐색 쿼리 등이 대표적인 사용 사례입니다.
-- recursive_org_chart.sql
WITH RECURSIVE org_hierarchy AS (
-- Base case: top-level managers (no manager above them)
SELECT
employee_id,
employee_name,
manager_id,
1 AS depth,
employee_name AS management_chain
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive case: join employees to their managers
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
oh.depth + 1,
oh.management_chain || ' > ' || e.employee_name
FROM employees e
INNER JOIN org_hierarchy oh ON e.manager_id = oh.employee_id
)
SELECT
employee_id,
employee_name,
depth,
management_chain
FROM org_hierarchy
ORDER BY management_chain;베이스 케이스는 루트 노드(상위 관리자가 없는 직원)를 선택합니다. 재귀 케이스는 CTE 자신에 다시 조인하여 계층을 레벨별로 구축합니다. management_chain 열은 이름을 연결하여 전체 보고 경로를 표시합니다. 대부분의 데이터베이스는 무한 루프를 방지하기 위해 재귀 깊이를 제한하며, PostgreSQL의 기본값은 100회입니다.
재귀 CTE는 대규모 데이터셋에서 처리 속도가 느려질 수 있습니다. 반드시 깊이 제한(WHERE depth < 10)을 포함하고, 조인 열(manager_id)에 인덱스가 설정되어 있는지 확인해야 합니다. 매우 깊은 계층 구조의 경우, 구체화된 경로(Materialized Path) 또는 중첩 집합(Nested Set) 패턴 도입을 검토하는 것이 좋습니다.
고급 분석 패턴: 갭과 아일랜드, 누적 합계
갭과 아일랜드 패턴은 데이터 내의 연속적인 시퀀스를 식별합니다. 활성 구독 기간, 연속 로그인 일수, 중단 없는 가동 기간 등이 해당됩니다. 이 기법은 ROW_NUMBER와 날짜 연산을 결합합니다.
-- consecutive_login_streaks.sql
WITH login_days AS (
SELECT DISTINCT
user_id,
DATE(login_timestamp) AS login_date
FROM user_sessions
),
streaks AS (
SELECT
user_id,
login_date,
-- Subtracting row_number from date creates a constant for consecutive days
login_date - (ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY login_date
))::int AS streak_group
FROM login_days
)
SELECT
user_id,
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_length
FROM streaks
GROUP BY user_id, streak_group
HAVING COUNT(*) >= 3
ORDER BY streak_length DESC;이 기법의 핵심 원리는 다음과 같습니다. 연속된 날짜에서 증가하는 행 번호를 빼면 항상 동일한 값이 산출됩니다. 갭이 발생하면 결과 값이 변경되어 새로운 그룹이 형성됩니다. 이 쿼리는 사용자별로 3일 이상의 연속 로그인 스트릭을 모두 검출합니다.
윈도우 함수와 CASE 식의 결합을 통한 조건부 분석
실무 분석 쿼리에서는 윈도우 함수와 CASE 식을 결합하여 동일한 쿼리 내에서 조건부 메트릭을 계산하는 경우가 빈번합니다.
-- conditional_analytics.sql
SELECT
order_date,
product_category,
amount,
-- 7-day moving average
AVG(amount) OVER (
PARTITION BY product_category
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d,
-- Cumulative sum resetting each quarter
SUM(amount) OVER (
PARTITION BY product_category, DATE_TRUNC('quarter', order_date)
ORDER BY order_date
) AS qtd_cumulative,
-- Flag if current row exceeds 2x the category average
CASE
WHEN amount > 2 * AVG(amount) OVER (PARTITION BY product_category)
THEN 'OUTLIER'
ELSE 'NORMAL'
END AS outlier_flag
FROM daily_sales
ORDER BY product_category, order_date;이 단일 쿼리로 7일 이동 평균, 분기별 리셋 누적 합계, 이상치 플래그를 동시에 계산할 수 있습니다. 서브쿼리나 셀프 조인이 전혀 필요하지 않습니다.
윈도우 함수는 WHERE, GROUP BY, HAVING 이후에 실행되며, ORDER BY와 LIMIT 이전에 실행됩니다. 따라서 WHERE 절에서 윈도우 함수를 직접 사용할 수 없습니다. 윈도우 함수 결과로 필터링하려면 쿼리를 CTE 또는 서브쿼리로 래핑해야 합니다.
분석 쿼리의 성능 최적화
윈도우 함수와 CTE는 강력한 도구이지만 대규모 테이블에서는 병목이 될 수 있습니다. 규모에 관계없이 성능을 유지하기 위한 주요 기법을 소개합니다.
첫째, PARTITION BY 및 ORDER BY 절에서 사용되는 열에 인덱스를 생성합니다. 윈도우 정의와 일치하는 복합 인덱스가 있으면 정렬 연산을 생략할 수 있습니다.
둘째, 윈도우 함수를 적용하기 전에 데이터를 필터링합니다. WHERE 절(윈도우 함수 평가 전에 실행됨)에 조건을 배치하면 윈도우가 처리해야 하는 데이터셋 크기를 줄일 수 있습니다.
셋째, 중복된 윈도우 정의를 피합니다. 명명된 윈도우를 사용하면 반복을 줄이고, 옵티마이저에게 여러 함수가 동일한 프레임을 공유한다는 것을 알릴 수 있습니다.
-- named_window.sql
SELECT
department,
employee_id,
salary,
RANK() OVER dept_window AS dept_rank,
SUM(salary) OVER dept_window AS dept_total,
AVG(salary) OVER dept_window AS dept_avg
FROM employees
WINDOW dept_window AS (
PARTITION BY department
ORDER BY salary DESC
);WINDOW 절(PostgreSQL, MySQL 8 이상, BigQuery에서 지원)은 윈도우를 한 번 정의하고 여러 함수에서 재사용할 수 있게 합니다. 가독성 향상과 쿼리 플랜 최적화에 모두 기여합니다.
Data Analytics 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
결론
- 윈도우 함수(ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE)는 GROUP BY 집계와 달리 행을 축소하지 않고 행 집합에 대해 계산을 수행합니다
- CTE는 복잡한 쿼리를 명명된, 테스트 가능한 단계로 분해하고 깊이 중첩된 서브쿼리를 대체합니다
- 재귀 CTE는 조직도나 카테고리 트리와 같은 계층 데이터를 처리하지만, 깊이 제한과 인덱스된 조인 열이 필요합니다
- 갭과 아일랜드 기법(ROW_NUMBER + 날짜 연산)은 시계열 데이터의 연속 시퀀스를 식별합니다
- 명명된 윈도우(WINDOW 절)는 코드 중복을 줄이고 쿼리 플랜 최적화를 개선할 수 있습니다
- WHERE 절에서 윈도우 함수 평가 전에 데이터를 필터링하여 대규모 테이블에서의 성능을 유지해야 합니다
- 이러한 패턴은 데이터 분석가 면접에서 빈번하게 출제되며, 일상적인 리포팅, 세분화, 트렌드 분석에도 직접 활용됩니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

2026년 데이터 애널리틱스 면접 질문 TOP 25
2026년 데이터 애널리틱스 면접 대비 가이드입니다. SQL, Python, Power BI, 통계, 행동 면접에서 자주 출제되는 25개 질문을 코드 예시와 함께 상세히 해설합니다.

Power BI vs Tableau 2026년 완벽 비교: 데이터 분석가가 배워야 할 도구는?
2026년 Power BI와 Tableau를 가격, AI 기능, 시각화 성능, 커리어 가치 측면에서 비교 분석합니다. 데이터 분석 전문가를 위한 BI 도구 선택 가이드입니다.

2026년 데이터 사이언스 면접 질문 25선
통계, 머신러닝, 피처 엔지니어링, 딥러닝, SQL, 시스템 설계를 망라하는 데이터 사이언스 면접 질문 25선 — Python 코드 예제와 심층 해설 포함.