🗄️ 데이터베이스

윈도우 함수

Window Function

그룹 내 각 행에 대해 집계 결과를 계산하면서도 개별 행을 유지하는 SQL 함수입니다. GROUP BY와 달리 행을 집계하지 않고, OVER 절로 정의된 윈도우(행 집합) 내에서 계산합니다.

📖 상세 설명

윈도우 함수(Window Function)는 행 그룹에 대한 계산을 수행하면서도 개별 행을 유지하는 SQL 함수입니다. GROUP BY는 그룹당 한 행으로 집계하지만, 윈도우 함수는 모든 행을 보존하면서 각 행에 계산된 값을 추가합니다.

OVER 절을 통해 윈도우를 정의합니다. PARTITION BY로 그룹을 나누고(선택적), ORDER BY로 정렬 순서를 지정합니다. 또한 ROWSRANGE로 윈도우 프레임(계산에 포함할 행 범위)을 세밀하게 조절할 수 있습니다.

대표적인 윈도우 함수: 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는 연속 순위를 부여합니다."

⚠️ 주의사항 & 베스트 프랙티스

WHERE 절에서 직접 사용 불가

윈도우 함수는 SELECT, ORDER BY에서만 사용 가능합니다. WHERE에서 필터링하려면 서브쿼리나 CTE로 감싸야 합니다.

PARTITION BY 없이 대량 데이터 처리

PARTITION BY 없이 사용하면 전체 데이터가 하나의 윈도우가 되어 메모리 사용량이 급증할 수 있습니다. 가능하면 적절히 분할하세요.

프레임 기본값 주의

ORDER BY를 지정하면 기본 프레임이 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW입니다. 의도와 다를 수 있으니 명시적으로 지정하세요.

인덱스 활용

PARTITION BY, ORDER BY에 사용되는 컬럼에 인덱스가 있으면 성능이 크게 향상됩니다. 특히 대용량 데이터에서는 필수입니다.

🔗 관련 용어

📚 더 배우기