1. 개요

“부서별 순위를 매기고 싶은데 각 사원 행은 그대로 남기고 싶다” 는 요구를 GROUP BY 로 처리하려다 막힌 적이 있을 것이다. GROUP BY 는 여러 행을 그룹당 한 행으로 줄여버려서 개별 사원 정보가 사라진다. 이럴 때 쓰는 게 윈도우 함수다.


2. 핵심 내용

2-1. GROUP BY 와 무엇이 다른가

GROUP BY 는 행을 그룹으로 묶고 집계해서 한 행으로 압축한다.

SELECT department, AVG(salary)
FROM employees
GROUP BY department;
-- 부서 수만큼만 행이 나온다

윈도우 함수는 행을 그룹으로 나누는 것까지는 같지만(PARTITION BY), 압축하지 않고 원래 행 수를 그대로 유지하면서 각 행에 그룹 기준의 계산 결과를 붙여준다.

SELECT name, department, salary,
       AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;
-- 사원 수만큼 행이 그대로 나오고, 각 행에 자기 부서 평균이 붙는다

PARTITION BY 는 GROUP BY 와 비슷하게 그룹을 나누는 역할이고, ORDER BY 는 그 그룹 안에서 순서를 정하는 역할이다.

2-2. RANK, DENSE_RANK, ROW_NUMBER

셋 다 순위를 매기지만 동점 처리 방식이 다르다.

SELECT name, department, salary,
       RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rnk,
       ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num
FROM employees;

같은 부서에 연봉이 같은 사원이 둘 있다고 하면(공동 1등):

  • RANK(): 1, 1, 3 — 동점자만큼 다음 순위를 건너뛴다.
  • DENSE_RANK(): 1, 1, 2 — 순위를 건너뛰지 않는다.
  • ROW_NUMBER(): 1, 2, 3 — 동점이어도 무조건 서로 다른 번호를 매긴다. “부서별 연봉 1등 한 명만 뽑기” 처럼 동점자 중 아무나 하나만 골라야 할 때 유용하다.

2-3. 누적합과 이동 평균

ORDER BY 와 프레임(ROWS BETWEEN ...)을 조합하면 누적합이나 이동 평균도 쉽게 만든다.

SELECT order_date, amount,
       SUM(amount) OVER (
           ORDER BY order_date
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_total
FROM orders;

UNBOUNDED PRECEDING AND CURRENT ROW 는 “맨 처음 행부터 현재 행까지” 라는 뜻이라, 날짜순으로 하나씩 더해가는 누적 매출이 그대로 나온다. 같은 방식으로 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 를 쓰면 최근 7일 이동 평균도 만들 수 있다. 예전 같으면 이런 계산을 애플리케이션이 전체 행을 읽어와서 루프를 돌리며 계산했는데, 윈도우 함수를 쓰면 DB 안에서 한 번에 끝난다.


3. 마무리

요약

  • GROUP BY 는 그룹당 한 행으로 압축하지만, 윈도우 함수는 원래 행 수를 유지한 채 계산 결과를 각 행에 붙인다.
  • PARTITION BY 는 그룹을 나누고, ORDER BY 는 그 안에서 순서를 정한다.
  • RANK 는 동점자만큼 순위를 건너뛰고, DENSE_RANK 는 안 건너뛰고, ROW_NUMBER 는 동점이어도 무조건 번호가 다르다.
  • 프레임(ROWS BETWEEN)을 조합하면 누적합, 이동 평균도 DB 안에서 바로 계산된다.

다음은 N+1 문제는 SQL 문제가 아니다 이다.