윈도우 함수
Window Function
그룹 내 각 행에 대해 집계 결과를 계산하면서도 개별 행을 유지하는 SQL 함수입니다. GROUP BY와 달리 행을 집계하지 않고, OVER 절로 정의된 윈도우(행 집합) 내에서 계산합니다.
Window Function
그룹 내 각 행에 대해 집계 결과를 계산하면서도 개별 행을 유지하는 SQL 함수입니다. GROUP BY와 달리 행을 집계하지 않고, OVER 절로 정의된 윈도우(행 집합) 내에서 계산합니다.
윈도우 함수(Window Function)는 행 그룹에 대한 계산을 수행하면서도 개별 행을 유지하는 SQL 함수입니다. GROUP BY는 그룹당 한 행으로 집계하지만, 윈도우 함수는 모든 행을 보존하면서 각 행에 계산된 값을 추가합니다.
OVER 절을 통해 윈도우를 정의합니다. PARTITION BY로 그룹을 나누고(선택적), ORDER BY로 정렬 순서를 지정합니다. 또한 ROWS나 RANGE로 윈도우 프레임(계산에 포함할 행 범위)을 세밀하게 조절할 수 있습니다.
대표적인 윈도우 함수: ROW_NUMBER(연속 순번), RANK(동점 시 같은 순위, 다음 순위 건너뜀), DENSE_RANK(동점 시 같은 순위, 다음 순위 연속), LAG/LEAD(이전/다음 행 값 참조), SUM/AVG/COUNT OVER(누적/이동 집계), FIRST_VALUE/LAST_VALUE(첫/마지막 값) 등이 있습니다.
순위 계산, 누적 합계, 이동 평균, 행 간 비교, 비율 계산 등 분석 작업에 필수적이며, 서브쿼리 없이 한 번의 테이블 스캔으로 복잡한 계산을 수행할 수 있어 성능 면에서도 유리합니다.
-- ============================================
-- 순위 함수: ROW_NUMBER, RANK, DENSE_RANK
-- ============================================
-- ROW_NUMBER: 연속 순번 부여
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) as overall_rank,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as dept_rank
FROM employees;
-- RANK vs DENSE_RANK 비교 (동점 처리)
-- salary: 100, 100, 90 일 때
-- RANK: 1, 1, 3 (동점 후 순위 건너뜀)
-- DENSE_RANK: 1, 1, 2 (동점 후 순위 연속)
SELECT
name,
salary,
RANK() OVER (ORDER BY salary DESC) as rank,
DENSE_RANK() OVER (ORDER BY salary DESC) as dense_rank
FROM employees;
-- ============================================
-- LAG / LEAD: 이전/다음 행 참조
-- ============================================
SELECT
order_date,
revenue,
LAG(revenue, 1, 0) OVER (ORDER BY order_date) as prev_day,
revenue - LAG(revenue, 1) OVER (ORDER BY order_date) as daily_change,
LEAD(revenue, 1) OVER (ORDER BY order_date) as next_day
FROM daily_sales;
-- ============================================
-- 누적 합계 (Running Total)
-- ============================================
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) as running_total,
SUM(amount) OVER () as grand_total, -- 전체 합계
ROUND(amount * 100.0 / SUM(amount) OVER (), 2) as pct_of_total
FROM orders;
-- ============================================
-- 윈도우 프레임: 이동 평균
-- ============================================
SELECT
order_date,
amount,
-- 최근 3일 이동 평균 (현재 행 포함 앞 2행)
AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) as moving_avg_3days,
-- 전후 1일 포함 평균
AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) as centered_avg
FROM daily_sales;
-- ============================================
-- 실무 분석 쿼리 예제
-- ============================================
-- 1. 부서별 Top 3 급여자 추출
WITH ranked AS (
SELECT
name,
department,
salary,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rn
FROM employees
)
SELECT * FROM ranked WHERE rn <= 3;
-- 2. 고객별 최근 주문 3개
SELECT * FROM (
SELECT
customer_id,
order_id,
order_date,
total_amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) as rn
FROM orders
) t WHERE rn <= 3;
-- 3. 월별 매출 + 전월 대비 증감률
SELECT
DATE_TRUNC('month', order_date) as month,
SUM(amount) as revenue,
LAG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)) as prev_month,
ROUND(
(SUM(amount) - LAG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)))
* 100.0 / NULLIF(LAG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)), 0),
2
) as growth_rate_pct
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
-- 4. 부서별 급여 분포 (본인 vs 부서 평균/최대)
SELECT
name,
department,
salary,
ROUND(AVG(salary) OVER (PARTITION BY department), 0) as dept_avg,
MAX(salary) OVER (PARTITION BY department) as dept_max,
salary - AVG(salary) OVER (PARTITION BY department) as diff_from_avg,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) as top_earner
FROM employees;
-- 5. 제품별 카테고리 내 순위 + 전체 순위
SELECT
product_name,
category,
sales,
RANK() OVER (PARTITION BY category ORDER BY sales DESC) as category_rank,
RANK() OVER (ORDER BY sales DESC) as overall_rank,
NTILE(4) OVER (ORDER BY sales DESC) as quartile -- 4분위
FROM product_sales;
# pandas에서 윈도우 함수 구현
import pandas as pd
# 샘플 데이터
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'department': ['Sales', 'Sales', 'IT', 'IT', 'IT'],
'salary': [60000, 55000, 70000, 65000, 70000]
})
# ROW_NUMBER: 그룹별 순번
df['dept_row_num'] = df.groupby('department').cumcount() + 1
# RANK: 그룹별 순위 (동점 시 같은 순위)
df['dept_rank'] = df.groupby('department')['salary'].rank(method='min', ascending=False)
# DENSE_RANK
df['dept_dense_rank'] = df.groupby('department')['salary'].rank(method='dense', ascending=False)
# LAG: 이전 행 값
df_sorted = df.sort_values(['department', 'salary'], ascending=[True, False])
df_sorted['prev_salary'] = df_sorted.groupby('department')['salary'].shift(1)
# 누적 합계 (Running Total)
df['cumsum_salary'] = df.groupby('department')['salary'].cumsum()
# 이동 평균 (Rolling)
sales_data = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10),
'amount': [100, 120, 90, 110, 130, 100, 140, 120, 150, 130]
})
sales_data['moving_avg_3'] = sales_data['amount'].rolling(window=3).mean()
# 전체 대비 비율
df['pct_of_total'] = df['salary'] / df['salary'].sum() * 100
df['pct_of_dept'] = df['salary'] / df.groupby('department')['salary'].transform('sum') * 100
print("=== 부서별 순위 ===")
print(df[['name', 'department', 'salary', 'dept_rank', 'dept_dense_rank']])
print("\n=== 누적 합계 ===")
print(df[['name', 'department', 'salary', 'cumsum_salary']])
print("\n=== 이동 평균 ===")
print(sales_data[['date', 'amount', 'moving_avg_3']])
"고객별로 최근 주문 3개만 가져와야 하는데요."
"ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC)로 순번 매기고, WHERE rn <= 3으로 필터링하면 됩니다. GROUP BY와 LIMIT 조합으로는 안 돼요."
"이 쿼리에서 전체 평균이랑 부서 평균을 서브쿼리 두 개로 따로 구하고 있어요."
"윈도우 함수로 한 번에 해결할 수 있어요. AVG(salary) OVER()는 전체 평균, AVG(salary) OVER(PARTITION BY dept)는 부서 평균입니다. 테이블 스캔이 3번에서 1번으로 줄어요."
"RANK와 DENSE_RANK의 차이점을 설명해주세요."
"값이 동일할 때 차이가 납니다. 예를 들어 점수가 100, 100, 90이면 RANK는 1, 1, 3이고 DENSE_RANK는 1, 1, 2입니다. RANK는 동점 수만큼 다음 순위를 건너뛰고, DENSE_RANK는 연속 순위를 부여합니다."
윈도우 함수는 SELECT, ORDER BY에서만 사용 가능합니다. WHERE에서 필터링하려면 서브쿼리나 CTE로 감싸야 합니다.
PARTITION BY 없이 사용하면 전체 데이터가 하나의 윈도우가 되어 메모리 사용량이 급증할 수 있습니다. 가능하면 적절히 분할하세요.
ORDER BY를 지정하면 기본 프레임이 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW입니다. 의도와 다를 수 있으니 명시적으로 지정하세요.
PARTITION BY, ORDER BY에 사용되는 컬럼에 인덱스가 있으면 성능이 크게 향상됩니다. 특히 대용량 데이터에서는 필수입니다.