SQL window functions: 순위 매기기, 누적 합계, 분석
Published: 2026-03-24
window functions는 현재 행과 관련된 행 집합에 대해 계산을 수행하면서도, GROUP BY처럼 결과를 단일 행으로 축소하지 않습니다. 이는 현대 SQL에서 가장 강력한 기능 중 하나로, SQLite(3.25+), PostgreSQL, MySQL(8.0+), SQL Server, Oracle에서 지원됩니다.
샘플 데이터
모든 예제에서 두 개의 테이블을 사용합니다.
employees 테이블:
| id | name | department | hire_date | salary |
|---|---|---|---|---|
| 1 | Alice | Engineering | 2023-01-15 | 95000 |
| 2 | Bob | Engineering | 2023-06-01 | 85000 |
| 3 | Carol | Engineering | 2024-03-10 | 85000 |
| 4 | Dave | Sales | 2023-02-20 | 72000 |
| 5 | Eve | Sales | 2023-09-05 | 68000 |
| 6 | Frank | Sales | 2024-01-15 | 65000 |
| 7 | Grace | Marketing | 2023-04-01 | 82000 |
| 8 | Henry | Marketing | 2024-06-01 | 70000 |
monthly_sales 테이블:
| id | employee_id | month | revenue |
|---|---|---|---|
| 1 | 1 | 2026-01 | 22000 |
| 2 | 1 | 2026-02 | 25000 |
| 3 | 1 | 2026-03 | 28000 |
| 4 | 2 | 2026-01 | 19000 |
| 5 | 2 | 2026-02 | 21000 |
| 6 | 2 | 2026-03 | 20000 |
| 7 | 3 | 2026-01 | 15000 |
| 8 | 3 | 2026-02 | 17000 |
| 9 | 3 | 2026-03 | 19000 |
| 10 | 4 | 2026-01 | 18000 |
| 11 | 4 | 2026-02 | 22000 |
| 12 | 4 | 2026-03 | 17000 |
| 13 | 5 | 2026-01 | 14000 |
| 14 | 5 | 2026-02 | 16000 |
| 15 | 5 | 2026-03 | 19000 |
| 16 | 6 | 2026-01 | 11000 |
| 17 | 6 | 2026-02 | 13000 |
| 18 | 6 | 2026-03 | 15000 |
| 19 | 7 | 2026-01 | 9000 |
| 20 | 7 | 2026-02 | 12000 |
| 21 | 7 | 2026-03 | 14000 |
| 22 | 8 | 2026-01 | 7000 |
| 23 | 8 | 2026-02 | 9000 |
| 24 | 8 | 2026-03 | 11000 |
window functions 문법
모든 window functions는 OVER 절을 사용해 어떤 행들에 대해 연산할지 정의합니다:
function_name(...) OVER (
[PARTITION BY column]
[ORDER BY column]
[frame_clause]
)
- PARTITION BY — 행을 그룹으로 나눕니다(GROUP BY와 비슷하지만, 모든 행을 유지)
- ORDER BY — 각 파티션 내에서 행의 정렬 순서를 정의
- 프레임 절(Frame clause) — 현재 행을 기준으로 윈도를 특정 범위의 행으로 제한
ROW_NUMBER()
ROW_NUMBER()는 각 파티션 내의 각 행에 고유한 연속 번호를 부여합니다. 번호는 항상 1부터 시작하며, 건너뛰거나 동점이 없습니다.
예제: 모든 직원에게 급여 순으로 번호 매기기
예제: 부서별로 직원에게 번호 매기기
PARTITION BY를 사용하면, 각 부서마다 번호가 1부터 다시 시작합니다:
RANK()와 DENSE_RANK()
RANK()와 DENSE_RANK()는 ROW_NUMBER()와 달리 동점을 다루는 방식이 다릅니다:
- ROW_NUMBER() — 항상 고유, 동점 시 임의의 순서
- RANK() — 같은 값은 같은 순위를 갖고, 그 다음 순위는 건너뜀 (1, 2, 2, 4)
- DENSE_RANK() — 같은 값은 같은 순위를 갖고, 번호를 건너뛰지 않음 (1, 2, 2, 3)
예제: 세 가지 순위 함수 비교
Bob과 Carol은 모두 85000을 받습니다 — 각 함수가 동점을 어떻게 처리하는지 확인해 보세요:
예제: 부서 내에서 직원 순위 매기기
NTILE()
NTILE(n)은 행들을 대략 동일한 크기의 n개 그룹(버킷)으로 나누고, 1부터 n까지 번호를 매깁니다.
예제: 직원을 급여 사분위수로 나누기
예제: 부서별로 직원을 두 개의 티어로 나누기
LAG()와 LEAD()
LAG(column, offset)은 이전 행의 값을 가져옵니다. LEAD(column, offset)은 다음 행의 값을 가져옵니다. 둘 다 기본 offset은 1이며, 해당 행이 없으면 NULL을 반환합니다.
예제: 전월 대비 매출 비교
각 직원에 대해 각 달의 매출을 이전 달과 비교합니다:
예제: 다음 달 매출 미리 보기
집계 window functions
표준 집계 함수(SUM, AVG, COUNT, MIN, MAX)는 OVER와 함께 window functions로 사용할 수 있습니다. GROUP BY와 달리, 출력에서 모든 행이 그대로 유지됩니다.
예제: 각 직원의 급여 vs. 부서 평균
예제: 전체 급여 대비 비율
각 직원이 전체 급여 총액에서 차지하는 비율을 계산합니다:
예제: 누적 합계와 누적 개수
파티션별 누적 합계
PARTITION BY와 ORDER BY를 결합해 그룹 내에서 누적 합계를 계산합니다.
예제: 직원별 누적 매출
예제: 전체 직원 기준 월별 매출 순위
각 달마다 직원의 순위를 구합니다:
윈도 프레임
윈도 프레임은 현재 행을 기준으로 계산에 포함될 행들을 정확히 정의합니다. 프레임 절은 OVER 안의 ORDER BY 뒤에 옵니다.
문법
ROWS BETWEEN <start> AND <end>
자주 쓰이는 경계:
UNBOUNDED PRECEDING— 파티션의 첫 행부터N PRECEDING— 현재 행에서 N행 이전까지CURRENT ROW— 현재 행N FOLLOWING— 현재 행에서 N행 이후까지UNBOUNDED FOLLOWING— 파티션의 마지막 행까지
예제: 2개월 이동 평균
예제: 누적 최소 및 최대 매출
FIRST_VALUE와 LAST_VALUE
FIRST_VALUE(column)은 윈도 프레임에서 첫 번째 값을 반환합니다. LAST_VALUE(column)은 마지막 값을 반환합니다. 주의할 점: 기본 프레임(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)에서 LAST_VALUE는 현재 행까지의 값만 볼 수 있습니다.
예제: 각 달을 첫 달과 비교하기
예제: 각 달을 최고 매출 달과 비교하기
전체 파티션을 보기 위해 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING을 사용합니다:
실전 예제: 부서별 최고 연봉자
자주 쓰이는 패턴: 서브쿼리에서 window functions를 사용한 뒤, 순위를 기준으로 필터링합니다.
예제: 각 부서에서 가장 높은 급여를 받는 직원
예제: 직원별 최고 매출 달
window functions vs. GROUP BY
핵심 차이점: GROUP BY는 행들을 요약 행으로 축소하는 반면, window functions는 모든 행을 유지하면서 계산된 열을 추가합니다.
예제: 나란히 비교하기
GROUP BY는 부서당 한 행을 반환합니다:
window functions는 모든 행을 유지합니다:
window functions의 일반적인 활용 사례
- 순위 및 리더보드 — 상위 N개 조회를 위한 ROW_NUMBER, RANK, DENSE_RANK
- 누적 합계 — 재무 및 재고 추적을 위한 누적 SUM
- 기간 간 비교 — 전월/전년 대비 분석을 위한 LAG/LEAD
- 이동 평균 — 프레임 기반 AVG로 데이터 스무딩
- 백분위 분석 — 데이터를 동일한 크기의 그룹으로 나누는 NTILE
- 중복 제거 — 그룹당 한 행을 선택하기 위한 ROW_NUMBER
- 갭 분석 — 누락된 시퀀스나 비정상적인 변화를 탐지하기 위한 LAG
성능 팁
- ORDER BY 컬럼에 인덱스 생성 — window functions는 내부적으로 정렬을 수행하므로, 인덱스가 도움이 됩니다.
- 파티션 수 최소화 — 파티션이 너무 많으면 연산량이 증가합니다.
- 윈도 정의 재사용 — 많은 데이터베이스가 동일한
OVER정의를 반복하지 않도록WINDOW절을 지원합니다. - 윈도 연산 후 필터링 — window functions는
WHERE이전에 실행되므로, 순위를 기준으로 필터링하려면 서브쿼리나 CTE를 사용하세요. - 불필요한 프레임 피하기 — 기본 동작으로 충분하다면 프레임 절을 생략하세요.
요약
- window functions는 결과 집합을 축소하지 않고, 관련된 행들에 걸쳐 값을 계산합니다.
- ROW_NUMBER, RANK, DENSE_RANK는 행에 서수(순번)를 부여합니다.
- LAG와 LEAD는 이전/다음 행의 값을 참조합니다.
- NTILE은 행을 동일한 크기의 버킷으로 나눕니다.
- FIRST_VALUE와 LAST_VALUE는 윈도에서 경계 값(처음/마지막)을 반환합니다.
OVER와 함께 사용하는 집계 함수(SUM, AVG, COUNT, MIN, MAX)는 그룹 수준 지표를 계산하면서도 모든 행을 유지합니다.- PARTITION BY는 독립적인 그룹을 만들고, ORDER BY는 행의 순서를 정의합니다.
- 윈도 프레임(
ROWS BETWEEN ... AND ...)은 포함할 행의 범위를 세밀하게 조정합니다. - window functions 결과(예: 그룹별 상위 N)를 기준으로 필터링하려면 서브쿼리를 사용하세요.
- SQLite 3.25+, PostgreSQL, MySQL 8.0+, SQL Server, Oracle에서 지원됩니다.
예제들을 수정해 보면서 서로 다른 파티션, 정렬, 프레임 절을 실험해 보고, window functions가 분석용 쿼리를 얼마나 단순하게 만들어 주는지 직접 확인해 보세요!