2과목 · SQL 기본과 활용 › 2-2. SQL 활용

윈도우 함수

윈도우 함수(Window Function)는 행과 행 사이의 관계를 이용해 순위, 누적합, 이동평균, 비율 등을 계산한다. GROUP BY와 달리 결과 행 수를 줄이지 않고, 원래 행마다 계산 값을 덧붙인다.

기본 문법

SELECT 윈도우함수(인수) OVER ( [PARTITION BY 컬럼]
                              [ORDER BY 컬럼 [ASC|DESC]]
                              [ROWS | RANGE 윈도우절] )
FROM   테이블;

OVER 절 안의 세 요소는 "나누고 → 줄 세우고 → 범위를 정한다" 순서로 읽으면 쉽다.

  • PARTITION BY 전체 행을 그룹으로 나눈다. 생략하면 전체가 하나의 파티션
  • ORDER BY 파티션 안에서 순서를 정한다. 순위 함수에서는 필수
  • 윈도우 절 계산 대상이 되는 행의 범위를 지정한다 (ROWS 또는 RANGE)

쓸 수 있는 위치

윈도우 함수는 SELECT 절과 ORDER BY 절에서만 쓸 수 있다. WHERE, GROUP BY, HAVING 절에서는 쓸 수 없다. 윈도우 함수 결과로 필터링하려면 인라인 뷰로 감싼 뒤 바깥 WHERE에서 조건을 건다.

예제 데이터

EMP 테이블 (전체 급여 합계 15000)

ENAME DEPTNO SAL
김민 10 3000
이준 10 2000
박솔 10 2000
최윤 20 4000
정하 20 3000
한결 20 1000

순위 함수 RANK, DENSE_RANK, ROW_NUMBER

SELECT ENAME, SAL,
       RANK()       OVER (ORDER BY SAL DESC) AS RNK,
       DENSE_RANK() OVER (ORDER BY SAL DESC) AS D_RNK,
       ROW_NUMBER() OVER (ORDER BY SAL DESC) AS RN
FROM   EMP;
ENAME SAL RNK D_RNK RN
최윤 4000 1 1 1
김민 3000 2 2 2
정하 3000 2 2 3
이준 2000 4 3 4
박솔 2000 4 3 5
한결 1000 6 4 6
  • RANK 동점은 같은 순위, 다음 순위는 동점 수만큼 건너뛴다
    • 1, 2, 2, 4
  • DENSE_RANK 동점은 같은 순위, 다음 순위는 건너뛰지 않는다
    • 1, 2, 2, 3
  • ROW_NUMBER 동점이어도 서로 다른 번호, 항상 연속
    • 1, 2, 3, 4
함수 동점 처리 다음 순위
RANK 같은 순위 부여 동점 수만큼 건너뜀 (1, 2, 2, 4)
DENSE_RANK 같은 순위 부여 건너뛰지 않음 (1, 2, 2, 3)
ROW_NUMBER 서로 다른 번호 부여 항상 연속 (1, 2, 3, 4)

ROW_NUMBER에서 동점인 김민과 정하 중 누가 2번을 받을지는 보장되지 않는다. 일정한 결과가 필요하면 ORDER BY SAL DESC, ENAME처럼 정렬 기준을 추가한다.

파티션별 순위

SELECT ENAME, DEPTNO, SAL,
       RANK() OVER (PARTITION BY DEPTNO ORDER BY SAL DESC) AS DEPT_RNK
FROM   EMP;
ENAME DEPTNO SAL DEPT_RNK
김민 10 3000 1
이준 10 2000 2
박솔 10 2000 2
최윤 20 4000 1
정하 20 3000 2
한결 20 1000 3

부서가 바뀌면 순위가 1부터 다시 시작한다.

시험 포인트: 동점이 있을 때 RANK는 순위를 건너뛰고, DENSE_RANK는 건너뛰지 않으며, ROW_NUMBER는 동점에도 고유 번호를 준다. 결과표에서 세 함수를 구분하는 문제가 자주 나온다.

집계 윈도우 함수

SUM, MAX, MIN, AVG, COUNT 뒤에 OVER 절을 붙이면 윈도우 함수로 동작한다.

SELECT ENAME, DEPTNO, SAL,
       SUM(SAL)   OVER (PARTITION BY DEPTNO) AS DEPT_SUM,
       MAX(SAL)   OVER (PARTITION BY DEPTNO) AS DEPT_MAX,
       COUNT(*)   OVER (PARTITION BY DEPTNO) AS DEPT_CNT
FROM   EMP;
ENAME DEPTNO SAL DEPT_SUM DEPT_MAX DEPT_CNT
김민 10 3000 7000 3000 3
이준 10 2000 7000 3000 3
박솔 10 2000 7000 3000 3
최윤 20 4000 8000 4000 3
정하 20 3000 8000 4000 3
한결 20 1000 8000 4000 3

ORDER BY 없이 PARTITION BY만 쓰면 파티션 전체가 계산 범위가 된다. 부서 최고 급여자만 뽑으려면 인라인 뷰에서 SAL = DEPT_MAX 조건을 건다.

윈도우 절 ROWS와 RANGE

윈도우 절은 "현재 행을 계산할 때 어느 행부터 어느 행까지 볼 것인가"를 정한다. 아래 그림은 SAL 오름차순으로 정렬한 6행에서 현재 행이 김민(3000)일 때, UNBOUNDED PRECEDING ~ CURRENT ROW 범위를 ROWS와 RANGE로 각각 잡은 모습이다.

파티션 전체 6행 · ORDER BY SAL 한결이준박솔김민정하최윤 100020002000300030004000 UNBOUNDED PRECEDINGCURRENT ROW동점 · RANGE만 포함범위 밖 ROWS 첫 행 ~ 현재 행 SUM = 8000 RANGE 현재 값(3000)과 같은 값까지 SUM = 11000
ROWS는 행 위치로 끊고, RANGE는 같은 값(동점)인 정하까지 함께 묶는다
  • ROWS 물리적인 행 위치 기준. 현재 행 기준 앞뒤 몇 행. 동점이어도 한 행씩 따로 센다.
  • RANGE ORDER BY 컬럼의 값 기준. 같은 값(동점)은 같은 범위로 묶인다.
키워드 의미
UNBOUNDED PRECEDING 파티션의 첫 행
n PRECEDING 현재 행 기준 n행 앞 (RANGE면 값이 n만큼 작은 지점)
CURRENT ROW 현재 행 (RANGE면 현재 값과 같은 값의 행 전체)
n FOLLOWING 현재 행 기준 n행 뒤 (RANGE면 값이 n만큼 큰 지점)
UNBOUNDED FOLLOWING 파티션의 마지막 행

ROWS UNBOUNDED PRECEDING처럼 시작점만 쓰면 끝점은 CURRENT ROW로 간주한다.

기본 윈도우 범위

윈도우 절을 생략하면

  • ORDER BY가 없으면 파티션 전체가 범위다.
  • ORDER BY가 있고 윈도우 절이 없으면 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW가 기본값이다.

누적합에서 ROWS와 RANGE 차이

SELECT ENAME, SAL,
       SUM(SAL) OVER (ORDER BY SAL) AS RANGE_SUM,   -- 기본: RANGE
       SUM(SAL) OVER (ORDER BY SAL
                      ROWS UNBOUNDED PRECEDING) AS ROWS_SUM
FROM   EMP;
ENAME SAL RANGE_SUM ROWS_SUM
한결 1000 1000 1000
이준 2000 5000 3000
박솔 2000 5000 5000
김민 3000 11000 8000
정하 3000 11000 11000
최윤 4000 15000 15000

RANGE는 동점(2000, 3000) 행을 한꺼번에 범위에 넣으므로 같은 값을 가진 행의 누적합이 같다. ROWS는 한 행씩 누적한다.

시험 포인트: ORDER BY만 있는 SUM() OVER (ORDER BY ...)는 RANGE 기준이라 동점 행의 누적합이 같게 나온다. 결과표에서 같은 값이 반복되면 RANGE, 계속 증가하면 ROWS로 판단한다.

이동 범위 예제

SELECT ENAME, SAL,
       SUM(SAL) OVER (ORDER BY SAL
                      ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS ROWS_3,
       SUM(SAL) OVER (ORDER BY SAL
                      RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING) AS RANGE_1000
FROM   EMP;
ENAME SAL ROWS_3 RANGE_1000
한결 1000 3000 5000
이준 2000 5000 11000
박솔 2000 7000 11000
김민 3000 8000 14000
정하 3000 10000 14000
최윤 4000 7000 10000
  • ROWS_3: 앞 1행 + 현재 행 + 뒤 1행. 첫 행과 마지막 행은 2행만 합산한다.
  • RANGE_1000: 현재 SAL ± 1000 범위의 값을 모두 합산한다. 예를 들어 SAL 3000이면 2000~4000 범위인 5개 행의 합 14000이다.

행 순서 함수 FIRST_VALUE, LAST_VALUE, LAG, LEAD

  • FIRST_VALUE 윈도우 범위의 첫 번째 값
  • LAST_VALUE 윈도우 범위의 마지막 값 (기본 범위에 주의)
  • LAG(컬럼, n, 기본값) n행 앞의 값을 가져온다
  • LEAD(컬럼, n, 기본값) n행 뒤의 값을 가져온다

FIRST_VALUE와 LAST_VALUE

SELECT ENAME, SAL,
       FIRST_VALUE(ENAME) OVER (ORDER BY SAL DESC) AS FIRST_NM,
       LAST_VALUE(ENAME)  OVER (ORDER BY SAL DESC) AS LAST_DEFAULT,
       LAST_VALUE(ENAME)  OVER (ORDER BY SAL DESC
                                ROWS BETWEEN UNBOUNDED PRECEDING
                                         AND UNBOUNDED FOLLOWING) AS LAST_ALL
FROM   EMP
WHERE  DEPTNO = 20;
ENAME SAL FIRST_NM LAST_DEFAULT LAST_ALL
최윤 4000 최윤 최윤 한결
정하 3000 최윤 정하 한결
한결 1000 최윤 한결 한결

LAST_VALUE 함정

기본 범위가 현재 행까지이므로 LAST_VALUE는 사실상 현재 행 값을 돌려준다. 파티션의 마지막 값을 얻으려면 끝점을 UNBOUNDED FOLLOWING으로 지정해야 한다.

LAG와 LEAD

LAG(컬럼, n, 기본값)은 n행 앞, LEAD(컬럼, n, 기본값)은 n행 뒤의 값을 가져온다. n의 기본값은 1이고, 가져올 행이 없으면 기본값(생략 시 NULL)을 돌려준다.

월별 매출 SALES: 1월 100, 2월 150, 3월 120, 4월 200

SELECT MON, AMT,
       LAG(AMT)        OVER (ORDER BY MON) AS PREV_AMT,
       LEAD(AMT)       OVER (ORDER BY MON) AS NEXT_AMT,
       LAG(AMT, 2, 0)  OVER (ORDER BY MON) AS PREV2,
       AMT - LAG(AMT)  OVER (ORDER BY MON) AS DIFF
FROM   SALES;
MON AMT PREV_AMT NEXT_AMT PREV2 DIFF
1월 100 NULL 150 0 NULL
2월 150 100 120 0 50
3월 120 150 200 100 -30
4월 200 120 NULL 150 80

비율 함수 RATIO_TO_REPORT, PERCENT_RANK, CUME_DIST, NTILE

SELECT ENAME, SAL,
       RATIO_TO_REPORT(SAL) OVER ()            AS RATIO,
       PERCENT_RANK() OVER (ORDER BY SAL DESC) AS P_RANK,
       CUME_DIST()    OVER (ORDER BY SAL DESC) AS C_DIST,
       NTILE(4)       OVER (ORDER BY SAL DESC) AS NT
FROM   EMP;
ENAME SAL RATIO P_RANK C_DIST NT
최윤 4000 0.2667 0 0.1667 1
김민 3000 0.2 0.2 0.5 1
정하 3000 0.2 0.2 0.5 2
이준 2000 0.1333 0.6 0.8333 2
박솔 2000 0.1333 0.6 0.8333 3
한결 1000 0.0667 1 1 4
함수 계산식 설명
RATIO_TO_REPORT 현재 값 / 파티션 합계 전체 대비 비중. NULL 값 제외. Oracle 전용, OVER 안에 ORDER BY 불가
PERCENT_RANK (RANK − 1) / (전체 행 수 − 1) 첫 행 0, 마지막 순위 1
CUME_DIST 현재 행 이하(동점 포함) 행 수 / 전체 행 수 누적 분포. 0보다 크고 1 이하
NTILE(n) 행을 n개 그룹으로 균등 분할 나머지는 앞 그룹부터 1개씩 더 배정

NTILE(4)로 6행 나누기

6 ÷ 4 = 1 나머지 2이다. 앞의 두 그룹이 2행씩, 뒤의 두 그룹이 1행씩 가져 그룹 크기는 2, 2, 1, 1이 된다.

시험 포인트: CUME_DIST는 동점 행을 모두 포함해 계산한다. SAL 3000인 두 행은 4000, 3000, 3000까지 3행이 포함되어 3/6 = 0.5다. NTILE은 나머지 행을 앞 그룹부터 배정한다는 점을 기억한다.

DBMS별 참고

  • SQL Server는 2012 버전부터 ROWS/RANGE 윈도우 절, FIRST_VALUE, LAST_VALUE, LAG, LEAD, PERCENT_RANK, CUME_DIST를 지원한다.
  • RATIO_TO_REPORT는 Oracle 전용이며 SQL Server에서는 SAL * 1.0 / SUM(SAL) OVER ()처럼 계산한다.
  • 순위 함수를 활용한 상위 N건 조회는 Top N 쿼리를 참고한다.

핵심 요약

  • 윈도우 함수는 OVER 절을 사용하며 결과 행 수를 줄이지 않는다.
  • RANK(1,2,2,4), DENSE_RANK(1,2,2,3), ROW_NUMBER(1,2,3,4)로 동점 처리가 다르다.
  • ORDER BY가 있으면 기본 범위는 RANGE UNBOUNDED PRECEDING부터 CURRENT ROW까지다.
  • ROWS는 행 위치, RANGE는 값 기준이며 RANGE는 동점을 같은 범위로 묶는다.
  • LAST_VALUE로 파티션 마지막 값을 얻으려면 UNBOUNDED FOLLOWING까지 범위를 넓힌다.
  • LAG는 이전 행, LEAD는 다음 행 값을 가져오며 세 번째 인수로 NULL 대체값을 준다.
  • PERCENT_RANK는 (순위−1)/(행수−1), CUME_DIST는 동점 포함 누적 비율이다.
  • 윈도우 함수는 WHERE 절에 쓸 수 없으므로 인라인 뷰로 감싸 필터링한다.

이 절 음성 강의 듣기