🗄️ 데이터베이스

윈도우 함수

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는 연속 순위를 부여합니다."

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

❌
WHERE 절에서 직접 사용 불가

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

❌
PARTITION BY 없이 대량 데이터 처리

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

❌
프레임 기본값 주의

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

✅
인덱스 활용

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

🔗 관련 용어

📚 더 배우기