SQL 윈도우 함수, GROUP BY와 뭐가 다를까
GROUP BY는 익숙한데 윈도우 함수는 계속 헷갈렸다. GROUP BY가 행을 줄이는 것과 달리 윈도우 함수는 행을 그대로 두고 계산 컬럼을 붙인다. ROW_NUMBER·LAG·누적합까지 실제 쓰는 것만 예제로 정리했다.
윈도우 함수를 오래 헷갈렸다. GROUP BY는 편한데 OVER (PARTITION BY ...)만 나오면 머리가 멈췄다. 그러다 GROUP BY랑 뭐가 다른지를 이해하고 나서야 좀 정리됐다.
GROUP BY는 행을 뭉쳐서 줄인다. 윈도우 함수는 행을 그대로 두고 계산 컬럼만 하나 붙인다.
예를 들어 직원 테이블에서 부서 평균 급여를 구한다고 하면,
GROUP BY dept→ 부서 수만큼 행이 나온다. 개별 직원 행은 사라진다.- 윈도우 → 직원 행을 다 유지한 채, 옆에 “그 직원 부서의 평균”을 붙인다.
SELECT name, dept, salary,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employees;
| name | dept | salary | dept_avg |
|---|---|---|---|
| Kim | Eng | 7000 | 7600 |
| Lee | Eng | 8200 | 7600 |
| Park | Sales | 5400 | 5750 |
| Choi | Sales | 6100 | 5750 |
dept_avg가 부서마다 같은 값으로 반복되는 게 포인트다. 행은 그대로, 컬럼만 붙었다.
OVER (...)= “이 창(window) 안에서 계산해라”PARTITION BY dept= 창을 부서별로 나눠라 (GROUP BY의 그룹과 비슷한 역할)- 창 안에서
ORDER BY= 순서가 필요한 계산(순위·누적·이전값)에 쓴다
순위: ROW_NUMBER / RANK / DENSE_RANK
셋 다 순위를 매기는데, 동점 처리만 다르다.
SELECT name, score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense
FROM players;
| score | row_num | rnk | dense |
|---|---|---|---|
| 90 | 1 | 1 | 1 |
| 80 | 2 | 2 | 2 |
| 80 | 3 | 2 | 2 |
| 70 | 4 | 4 | 3 |
- ROW_NUMBER: 무조건 1, 2, 3, 4. 동점도 임의로 갈라놓는다.
- RANK: 동점은 같은 순위, 다음은 건너뛴다(2, 2, 4).
- DENSE_RANK: 동점은 같은 순위, 안 건너뛴다(2, 2, 3).
내가 제일 자주 쓰는 건 “그룹별 top-N” 이다. GROUP BY로는 깔끔하게 안 되는 그거.
SELECT * FROM (
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn <= 2; -- 부서별 급여 상위 2명
부서마다 순위를 매기고(PARTITION BY dept), 바깥에서 rn <= 2로 자른다.
이전/다음 행: LAG / LEAD
전월 대비, 증감 같은 걸 잴 때 쓴다. LAG는 이전 행, LEAD는 다음 행 값을 가져온다.
SELECT month, revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change
FROM monthly_revenue;
| month | revenue | mom_change |
|---|---|---|
| 2026-01 | 100 | (null) |
| 2026-02 | 130 | 30 |
| 2026-03 | 120 | -10 |
첫 행은 이전 값이 없어서 null이다. 기본값이 필요하면 LAG(revenue, 1, 0)처럼 세 번째 인자를 준다.
누적합, 그리고 내가 자꾸 틀린 지점
SUM(amount) OVER (ORDER BY date) -- 누적합
여기서 오래 헤맸다. ORDER BY를 붙이면 기본 프레임이 “처음부터 현재 행까지” 로 잡힌다. 그래서 누적합이 된다. 그런데 ORDER BY 없이 PARTITION BY만 있으면 창 전체가 대상이라 그냥 총합이 나온다.
같은 SUM인데 ORDER BY 하나로 “총합”이냐 “누적합”이냐가 갈린다. 값이 이상하면 십중팔구 여기다.
연습 문제
방금 그 함정을 직접 밟아보는 게 제일 빠르다. 아래 orders 테이블이 있다.
| user_id | order_date | amount |
|---|---|---|
| 1 | 2026-01-02 | 10 |
| 1 | 2026-01-05 | 30 |
| 1 | 2026-01-11 | 20 |
| 2 | 2026-01-03 | 50 |
| 2 | 2026-01-09 | 40 |
문제: 각 유저별로 주문을 날짜순으로 보면서, 그 시점까지의 누적 결제액(cumulative)을 함께 출력하라. 유저 1은 10 → 40 → 60으로, 유저 2는 따로 50 → 90으로 쌓여야 한다.
한 번 직접 써보고, 아래를 펼쳐 두 쿼리를 비교해보자.
정답 보기
① 누적합 — ORDER BY가 있을 때 (정답)
SELECT user_id, order_date, amount,
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS cumulative
FROM orders
ORDER BY user_id, order_date;
| user_id | order_date | amount | cumulative |
|---|---|---|---|
| 1 | 2026-01-02 | 10 | 10 |
| 1 | 2026-01-05 | 30 | 40 |
| 1 | 2026-01-11 | 20 | 60 |
| 2 | 2026-01-03 | 50 | 50 |
| 2 | 2026-01-09 | 40 | 90 |
② 총합 — ORDER BY를 빼면
SELECT user_id, order_date, amount,
SUM(amount) OVER (PARTITION BY user_id) AS cumulative
FROM orders
ORDER BY user_id, order_date;
| user_id | order_date | amount | cumulative |
|---|---|---|---|
| 1 | 2026-01-02 | 10 | 60 |
| 1 | 2026-01-05 | 30 | 60 |
| 1 | 2026-01-11 | 20 | 60 |
| 2 | 2026-01-03 | 50 | 90 |
| 2 | 2026-01-09 | 40 | 90 |
차이는 ORDER BY order_date 한 줄뿐이다. 그거 하나로 cumulative가 날짜순 누적(10→40→60)이냐, 유저별 총합(전부 60)이냐로 갈린다.
정리
결국 윈도우 함수는 세 조각의 조합이다. 어떤 함수를 쓸지(순위/이전값/합), 창을 어떻게 나눌지(PARTITION BY), 창 안에서 순서가 필요한지(ORDER BY).
나는 이걸 눈으로만 읽을 땐 계속 헷갈렸는데, 직접 만든 SQL 드릴로 실제 테이블에 쿼리를 돌려보면서 손에 붙었다. 특히 누적합 프레임은 위 연습 문제처럼 한 번 틀려봐야 안 까먹는다.