윈도우 함수 심화
윈도우 함수의 진가는 순위 매기기를 넘어, 이전 행 참조, 누적합, 이동 평균 등 시계열 분석에서 발휘됩니다. 윈도우 함수는 GROUP BY처럼 행을 축약하지 않으면서 집계 결과를 사용할 수 있는 SQL의 핵심 고급 기능입니다.
LAG, LEAD — 이전/다음 행 참조
현재 행을 기준으로 이전(LAG) 또는 다음(LEAD) 행의 값을 참조합니다. 별도의 셀프 조인 없이 행 간 비교가 가능합니다.
LAG(컬럼, N, 기본값) OVER (
[PARTITION BY 그룹컬럼]
ORDER BY 정렬컬럼
)
-- N: 몇 행 전인지 (기본값 1)
-- 기본값: NULL 대신 사용할 값 (선택)SELECT month, revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
revenue - LAG(revenue, 1) OVER (ORDER BY month) AS growth
FROM monthly_sales;
-- 결과:
-- 1월 | 1000 | NULL | NULL
-- 2월 | 1200 | 1000 | 200
-- 3월 | 900 | 1200 | -300SELECT name, hire_date,
LEAD(hire_date, 1) OVER (ORDER BY hire_date) AS next_hire,
LEAD(hire_date, 1) OVER (ORDER BY hire_date) - hire_date AS gap_days
FROM employees;
-- 다음 입사까지 며칠 걸렸는지 계산-- 기본값 지정으로 NULL 방지
SELECT month, revenue,
LAG(revenue, 1, 0) OVER (ORDER BY month) AS prev_month
FROM monthly_sales;
-- 2행 전 참조
SELECT date, price,
LAG(price, 2) OVER (ORDER BY date) AS two_days_ago
FROM stock_prices;
-- 부서별 이전 사원 급여 비교
SELECT dept, name, salary,
salary - LAG(salary) OVER (PARTITION BY dept ORDER BY salary DESC)
AS diff_from_higher
FROM employees;셀프 조인 방식 (비효율)
SELECT a.month, a.revenue, b.revenue AS prev
FROM monthly_sales a
LEFT JOIN monthly_sales b ON a.month = b.month + 1
→ 조인 비용 발생, 복잡한 조건
윈도우 함수 방식 (효율)
SELECT month, revenue,
LAG(revenue) OVER (ORDER BY month) AS prev
FROM monthly_sales
→ 단일 테이블 스캔, 깔끔한 구문SUM/AVG OVER — 누적합, 이동 평균
ORDER BY를 포함한 윈도우 함수에서 SUM, AVG 등 집계 함수를 사용하면, 기본적으로 처음부터 현재 행까지의 범위에 대해 집계합니다.
SELECT month, revenue,
SUM(revenue) OVER (ORDER BY month) AS cumulative
FROM monthly_sales;
-- 결과:
-- 1월 | 1000 | 1000
-- 2월 | 1200 | 2200
-- 3월 | 900 | 3100
-- 4월 | 1500 | 4600SELECT dept, month, revenue,
SUM(revenue) OVER (
PARTITION BY dept ORDER BY month
) AS dept_cumulative
FROM dept_sales;
-- 부서별로 독립적인 누적합 계산SELECT month, revenue,
AVG(revenue) OVER (
ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM monthly_sales;
-- 결과:
-- 1월 | 1000 | 1000.0 ← 1개만 (이전 행 없음)
-- 2월 | 1200 | 1100.0 ← 2개 평균
-- 3월 | 900 | 1033.3 ← 3개 평균 (1000+1200+900)/3
-- 4월 | 1500 | 1200.0 ← 3개 평균 (1200+900+1500)/3SELECT dept, revenue,
SUM(revenue) OVER () AS total,
ROUND(revenue * 100.0 / SUM(revenue) OVER (), 1) AS pct
FROM dept_revenue;
-- SUM() OVER ()는 ORDER BY 없이 전체 합계
-- 결과: 영업 | 5000 | 12000 | 41.7%-- 부서별 입사 순서별 인원 누적
SELECT dept, name, hire_date,
COUNT(*) OVER (PARTITION BY dept ORDER BY hire_date) AS running_count,
SUM(salary) OVER (PARTITION BY dept ORDER BY hire_date) AS running_total
FROM employees;ROWS BETWEEN 프레임 지정
윈도우 프레임은 현재 행을 기준으로 집계에 포함할 행의 범위를 지정합니다. 기본값은 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW입니다.
ROWS BETWEEN 시작 AND 끝
시작/끝 옵션:
UNBOUNDED PRECEDING — 파티션의 첫 행
N PRECEDING — N행 전
CURRENT ROW — 현재 행
N FOLLOWING — N행 후
UNBOUNDED FOLLOWING — 파티션의 마지막 행
시각화:
행1 행2 행3 [행4] 행5 행6 행7
↑ 현재 행
←──────── PRECEDING ──── FOLLOWING ────────→
UNBOUNDED UNBOUNDED
PRECEDING FOLLOWING-- 앞뒤 1행을 포함한 이동 평균 (3행 윈도우)
AVG(revenue) OVER (
ORDER BY month
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
)
-- 처음부터 현재까지 누적합 (기본 프레임)
SUM(revenue) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
-- 현재부터 끝까지의 합
SUM(revenue) OVER (
ORDER BY month
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
)ROWS: 물리적 행 수 기준
→ ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
→ 정확히 이전 2행 + 현재 행
RANGE: 논리적 값 범위 기준
→ RANGE BETWEEN 2 PRECEDING AND CURRENT ROW
→ ORDER BY 컬럼 값이 현재 - 2 이상인 행
차이가 나는 경우:
ORDER BY 컬럼에 동일 값이 있을 때
ROWS: 물리적 순서에 따라 정확히 N행
RANGE: 같은 값의 행들을 모두 포함
Oracle, PostgreSQL은 RANGE 지원
MySQL 8.0+도 RANGE 지원-- 데이터: 1월=100, 2월=200, 2월=200, 3월=300
-- ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
-- 1행: 100 → SUM=100
-- 2행: 200 → SUM=300 (100+200)
-- 3행: 200 → SUM=400 (200+200) ← 물리적 이전 1행
-- 4행: 300 → SUM=500 (200+300)
-- RANGE BETWEEN 1 PRECEDING AND CURRENT ROW (ORDER BY month)
-- 1월: 100 → SUM=100
-- 2월: 200 → SUM=500 (100+200+200) ← 1월~2월 범위
-- 2월: 200 → SUM=500 (100+200+200) ← 같은 결과
-- 3월: 300 → SUM=700 (200+200+300) ← 2월~3월 범위NTILE — N등분
NTILE(N)은 정렬된 결과를 N개의 동일 크기 그룹으로 분할합니다. 행 수가 N으로 정확히 나누어지지 않으면 앞 그룹부터 1행씩 더 배분합니다.
-- 급여 기준 4분위
SELECT name, salary,
NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
-- 결과 (8명 기준):
-- 김과장 | 9000 | 1 (상위 25%)
-- 이과장 | 8000 | 1
-- 박대리 | 7000 | 2
-- 최대리 | 6000 | 2
-- 정사원 | 5000 | 3
-- 한사원 | 4000 | 3
-- 오인턴 | 3000 | 4
-- 서인턴 | 2000 | 4 (하위 25%)-- 10분위 (Decile) 분류
SELECT name, score,
NTILE(10) OVER (ORDER BY score DESC) AS decile
FROM test_results;
-- 성과 등급 자동 부여
SELECT name, sales,
CASE NTILE(5) OVER (ORDER BY sales DESC)
WHEN 1 THEN 'S'
WHEN 2 THEN 'A'
WHEN 3 THEN 'B'
WHEN 4 THEN 'C'
WHEN 5 THEN 'D'
END AS grade
FROM employee_sales;FIRST_VALUE, LAST_VALUE
파티션 내 첫 번째 또는 마지막 행의 값을 가져옵니다.
SELECT dept, name, salary,
FIRST_VALUE(name) OVER (
PARTITION BY dept ORDER BY salary DESC
) AS highest_earner,
LAST_VALUE(name) OVER (
PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS lowest_earner
FROM employees;
-- LAST_VALUE 주의: 기본 프레임이 CURRENT ROW까지이므로
-- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 명시 필수!-- Oracle, PostgreSQL, MySQL 8.0+ 지원
SELECT dept, name, salary,
NTH_VALUE(name, 2) OVER (
PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS second_highest
FROM employees;
-- 각 부서에서 두 번째로 높은 급여를 받는 사람PERCENT_RANK, CUME_DIST
백분위 순위를 계산하는 함수입니다.
-- 0~1 사이 값: (순위 - 1) / (총 행 - 1)
SELECT name, salary,
RANK() OVER (ORDER BY salary) AS rnk,
ROUND(PERCENT_RANK() OVER (ORDER BY salary), 2) AS pct_rank
FROM employees;
-- 결과 (5명):
-- 최인턴 | 2000 | 1 | 0.00
-- 한사원 | 4000 | 2 | 0.25
-- 박대리 | 6000 | 3 | 0.50
-- 이과장 | 8000 | 4 | 0.75
-- 김부장 | 10000| 5 | 1.00-- 누적 분포: 현재 행 이하의 비율
SELECT name, salary,
ROUND(CUME_DIST() OVER (ORDER BY salary), 2) AS cum_dist
FROM employees;
-- 결과 (5명):
-- 최인턴 | 2000 | 0.20 ← 1/5
-- 한사원 | 4000 | 0.40 ← 2/5
-- 박대리 | 6000 | 0.60 ← 3/5
-- 이과장 | 8000 | 0.80 ← 4/5
-- 김부장 | 10000| 1.00 ← 5/5윈도우 함수 실행 순서
윈도우 함수는 SQL 실행 순서에서 SELECT 절에서 실행되며, WHERE/GROUP BY/HAVING 이후에 처리됩니다. 따라서 WHERE에서 윈도우 함수 결과를 사용할 수 없습니다.
1. FROM / JOIN → 테이블 결합
2. WHERE → 행 필터링
3. GROUP BY → 그룹화
4. HAVING → 그룹 필터링
5. SELECT → 컬럼 선택 + 윈도우 함수 실행 ★
6. ORDER BY → 정렬
7. LIMIT → 행 수 제한
→ 윈도우 함수는 5단계에서 실행
→ WHERE에서 윈도우 함수 사용 불가
→ 필터링이 필요하면 서브쿼리나 CTE 사용-- 오류! WHERE에서 윈도우 함수 사용 불가
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
WHERE rnk <= 3; -- ERROR!
-- 해결: 서브쿼리 또는 CTE 사용
WITH ranked AS (
SELECT name, salary,
RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT name, salary, rnk
FROM ranked
WHERE rnk <= 3;복합 윈도우 함수 패턴
-- 매출이 전월 대비 연속 증가한 구간
WITH growth AS (
SELECT month, revenue,
CASE WHEN revenue > LAG(revenue) OVER (ORDER BY month)
THEN 1 ELSE 0 END AS is_growth
FROM monthly_sales
),
grouped AS (
SELECT month, revenue, is_growth,
SUM(CASE WHEN is_growth = 0 THEN 1 ELSE 0 END)
OVER (ORDER BY month) AS grp
FROM growth
)
SELECT MIN(month) AS start_month, MAX(month) AS end_month,
COUNT(*) AS streak_length
FROM grouped
WHERE is_growth = 1
GROUP BY grp;SELECT date, value,
value - AVG(value) OVER (
ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS deviation,
CASE WHEN ABS(value - AVG(value) OVER (
ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)) > 2 * STDDEV(value) OVER (
ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) THEN 'OUTLIER' ELSE 'NORMAL' END AS status
FROM sensor_data;WITH product_sales AS (
SELECT product, SUM(amount) AS total
FROM sales
GROUP BY product
)
SELECT product, total,
SUM(total) OVER (ORDER BY total DESC) AS running_total,
ROUND(SUM(total) OVER (ORDER BY total DESC) * 100.0
/ SUM(total) OVER (), 1) AS cumulative_pct
FROM product_sales
ORDER BY total DESC;
-- 상위 20% 상품이 매출의 80%를 차지하는지 확인 (파레토 법칙)실습: 종합 매출 분석
SELECT month, revenue,
-- 전월 대비 성장률
ROUND((revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100, 1) AS growth_pct,
-- 누적 매출
SUM(revenue) OVER (ORDER BY month) AS cumulative,
-- 3개월 이동 평균
ROUND(AVG(revenue) OVER (
ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)) AS ma3,
-- 전체 비중
ROUND(revenue / SUM(revenue) OVER () * 100, 1) AS pct_of_total,
-- 4분위
NTILE(4) OVER (ORDER BY revenue) AS quartile
FROM monthly_sales;DBMS별 윈도우 함수 지원
| 기능 | Oracle | MySQL 8.0+ | PostgreSQL | SQL Server |
|---|---|---|---|---|
| ROW_NUMBER | ✓ | ✓ | ✓ | ✓ |
| RANK / DENSE_RANK | ✓ | ✓ | ✓ | ✓ |
| LAG / LEAD | ✓ | ✓ | ✓ | ✓ |
| NTILE | ✓ | ✓ | ✓ | ✓ |
| FIRST_VALUE / LAST_VALUE | ✓ | ✓ | ✓ | ✓ |
| NTH_VALUE | ✓ | ✓ | ✓ | ✗ |
| PERCENT_RANK | ✓ | ✓ | ✓ | ✓ |
| CUME_DIST | ✓ | ✓ | ✓ | ✓ |
| RANGE 프레임 | ✓ | ✓ | ✓ | ✓ |
| GROUPS 프레임 | ✗ | ✗ | ✓ | ✗ |
MySQL 5.7 이하: 윈도우 함수 미지원 → 서브쿼리로 대체
MySQL 8.0+: 대부분의 윈도우 함수 지원
Oracle: KEEP (DENSE_RANK FIRST/LAST) 등 추가 분석 함수 지원
PostgreSQL: FILTER 절 등 확장 기능 지원성능 고려사항
1. PARTITION BY에 인덱스 활용
→ PARTITION BY dept ORDER BY salary
→ dept 컬럼에 인덱스가 있으면 파티션 나누기가 빠름
2. ORDER BY 컬럼 인덱스
→ 정렬 비용 감소: ORDER BY 컬럼에 인덱스 존재 시 성능 향상
3. 불필요한 윈도우 함수 중복 제거
→ 동일 OVER 절을 여러 함수에서 사용하면
→ DBMS가 내부적으로 윈도우를 1회만 계산 (윈도우 공유)
→ 명시적으로 WINDOW 절을 사용하면 가독성도 향상
4. WINDOW 절 활용 (PostgreSQL, MySQL 8.0+)
SELECT name, salary,
SUM(salary) OVER w AS running_total,
AVG(salary) OVER w AS running_avg
FROM employees
WINDOW w AS (ORDER BY hire_date);
→ 동일 윈도우 정의를 재사용
5. 대규모 데이터셋에서 주의
→ 파티션 크기가 매우 크면 메모리/정렬 비용 증가
→ 필요 시 WHERE로 범위를 먼저 제한정리
윈도우 함수를 활용하면 셀프 조인이나 상관 서브쿼리 없이 행 간 비교, 누적 계산, 순위 분석을 효율적으로 처리할 수 있습니다. 핵심 포인트는 다음과 같습니다.
- LAG/LEAD는 이전/다음 행 참조로 시계열 비교에 필수
- SUM/AVG OVER로 누적합과 이동 평균을 한 번의 쿼리로 계산
- ROWS BETWEEN으로 정확한 윈도우 프레임 제어
- NTILE로 등급, PERCENT_RANK로 백분위 순위 계산
- 윈도우 함수는 SELECT 단계에서 실행되므로 WHERE에서 사용 불가
- LAST_VALUE 사용 시 프레임을 UNBOUNDED FOLLOWING까지 명시해야 정확한 결과
- ROWS는 물리적 행, RANGE는 논리적 값 범위 기준으로 프레임을 결정
다음 장에서는 테이블의 구조를 올바르게 설계하는 데이터 모델링과 ERD를 다루겠습니다.