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
DbGate Lite에서 데이터셋 탐색하기

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 BYORDER 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

성능 팁

  1. ORDER BY 컬럼에 인덱스 생성 — window functions는 내부적으로 정렬을 수행하므로, 인덱스가 도움이 됩니다.
  2. 파티션 수 최소화 — 파티션이 너무 많으면 연산량이 증가합니다.
  3. 윈도 정의 재사용 — 많은 데이터베이스가 동일한 OVER 정의를 반복하지 않도록 WINDOW 절을 지원합니다.
  4. 윈도 연산 후 필터링 — window functions는 WHERE 이전에 실행되므로, 순위를 기준으로 필터링하려면 서브쿼리나 CTE를 사용하세요.
  5. 불필요한 프레임 피하기 — 기본 동작으로 충분하다면 프레임 절을 생략하세요.

요약

  • window functions는 결과 집합을 축소하지 않고, 관련된 행들에 걸쳐 값을 계산합니다.
  • ROW_NUMBER, RANK, DENSE_RANK는 행에 서수(순번)를 부여합니다.
  • LAGLEAD는 이전/다음 행의 값을 참조합니다.
  • NTILE은 행을 동일한 크기의 버킷으로 나눕니다.
  • FIRST_VALUELAST_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가 분석용 쿼리를 얼마나 단순하게 만들어 주는지 직접 확인해 보세요!