행을 그룹으로 접지 않고, 각 행마다 정해진 범위(윈도우)를 보며 계산하는 함수.
GROUP BY와 무엇이 다른가
| GROUP BY | 여러 행을 한 행으로 '접는다' | → 원래 행 정보가 사라진다 |
|---|---|---|
| OVER | 행은 그대로 두고 옆을 '본다' | → 원래 행 + 계산 결과를 함께 얻는다 |
-- GROUP BY: 부서별 평균만 남는다
SELECT dept, avg(salary) FROM emp GROUP BY dept;
-- OVER: 각 사원 행에 그 부서 평균이 따라붙는다
SELECT name, dept, salary, avg(salary) OVER (PARTITION BY dept) AS dept_avg
FROM emp;
-- 김철수 | 개발 | 5000 | 5200
-- 이영희 | 개발 | 5400 | 5200 ← 개인 정보를 잃지 않는다
구조
함수() OVER (
PARTITION BY 그룹기준 -- 어떻게 나눌까 (생략하면 전체가 한 덩어리)
ORDER BY 정렬기준 -- 어떤 순서로 볼까
ROWS BETWEEN ... AND ... -- 어디까지 볼까 (프레임)
)
순위 세 형제 — 동점 처리가 다르다
SELECT name, score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
RANK() OVER (ORDER BY score DESC) AS rk,
DENSE_RANK() OVER (ORDER BY score DESC) AS dr
FROM student;
-- score 100, 100, 90 일 때
-- rn: 1, 2, 3 무조건 순번 (동점도 임의로 가른다)
-- rk: 1, 1, 3 동점은 같은 등수, 다음은 건너뛴다
-- dr: 1, 1, 2 동점은 같은 등수, 다음이 이어진다
그룹별 상위 N — 가장 흔한 용도
-- 부서별 연봉 상위 3명
SELECT * FROM (
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM emp
) t WHERE rn <= 3;
윈도우 함수는 WHERE에서 쓸 수 없다 — 실행 순서상 WHERE가 먼저 처리되기 때문이다. 그래서 서브쿼리나 CTE로 한 겹 감싼다.
누적과 이전 값 비교
-- 누적 매출
SELECT d, amount,
sum(amount) OVER (ORDER BY d ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum
FROM daily_sales;
-- 전일 대비 증감 — 이전 행의 값을 당겨 온다
SELECT d, amount,
amount - LAG(amount) OVER (ORDER BY d) AS diff
FROM daily_sales;
LAG(이전 행)·LEAD(다음 행)는 자기 조인 없이 시계열 비교를 하게 해 준다. 예전에는 같은 테이블을 하루 어긋나게 조인해야 했던 일이다.
지원 현황
- PostgreSQL — 8.4 부터 (오래됐다)
- MySQL — 8.0 부터 — 5.7 이하에서는 변수 트릭으로 흉내 내야 했다
면접 함정
- ❌ "윈도우 함수는 느리다" → 대안(자기 조인·상관 서브쿼리)이 대개 더 느리다. 정렬 비용은 인덱스로 줄인다.
- ❌ "RANK와 ROW_NUMBER는 같다" → 동점 처리가 다르다. 페이지네이션에는 유일성이 보장되는
ROW_NUMBER를 쓴다.