스터디

SQL 윈도우 함수, GROUP BY와 뭐가 다를까

GROUP BY는 익숙한데 윈도우 함수는 계속 헷갈렸다. GROUP BY가 행을 줄이는 것과 달리 윈도우 함수는 행을 그대로 두고 계산 컬럼을 붙인다. ROW_NUMBER·LAG·누적합까지 실제 쓰는 것만 예제로 정리했다.

#SQL#윈도우함수#스터디

윈도우 함수를 오래 헷갈렸다. 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;
namedeptsalarydept_avg
KimEng70007600
LeeEng82007600
ParkSales54005750
ChoiSales61005750

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;
scorerow_numrnkdense
90111
80222
80322
70443
  • 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;
monthrevenuemom_change
2026-01100(null)
2026-0213030
2026-03120-10

첫 행은 이전 값이 없어서 null이다. 기본값이 필요하면 LAG(revenue, 1, 0)처럼 세 번째 인자를 준다.

누적합, 그리고 내가 자꾸 틀린 지점

SUM(amount) OVER (ORDER BY date)   -- 누적합

여기서 오래 헤맸다. ORDER BY를 붙이면 기본 프레임이 “처음부터 현재 행까지” 로 잡힌다. 그래서 누적합이 된다. 그런데 ORDER BY 없이 PARTITION BY만 있으면 창 전체가 대상이라 그냥 총합이 나온다.

같은 SUM인데 ORDER BY 하나로 “총합”이냐 “누적합”이냐가 갈린다. 값이 이상하면 십중팔구 여기다.

연습 문제

방금 그 함정을 직접 밟아보는 게 제일 빠르다. 아래 orders 테이블이 있다.

user_idorder_dateamount
12026-01-0210
12026-01-0530
12026-01-1120
22026-01-0350
22026-01-0940

문제: 각 유저별로 주문을 날짜순으로 보면서, 그 시점까지의 누적 결제액(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_idorder_dateamountcumulative
12026-01-021010
12026-01-053040
12026-01-112060
22026-01-035050
22026-01-094090

② 총합 — 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_idorder_dateamountcumulative
12026-01-021060
12026-01-053060
12026-01-112060
22026-01-035090
22026-01-094090

차이는 ORDER BY order_date 한 줄뿐이다. 그거 하나로 cumulative가 날짜순 누적(10→40→60)이냐, 유저별 총합(전부 60)이냐로 갈린다.

정리

결국 윈도우 함수는 세 조각의 조합이다. 어떤 함수를 쓸지(순위/이전값/합), 창을 어떻게 나눌지(PARTITION BY), 창 안에서 순서가 필요한지(ORDER BY).

나는 이걸 눈으로만 읽을 땐 계속 헷갈렸는데, 직접 만든 SQL 드릴로 실제 테이블에 쿼리를 돌려보면서 손에 붙었다. 특히 누적합 프레임은 위 연습 문제처럼 한 번 틀려봐야 안 까먹는다.


← 다른 글 보기