숏컷 코드
SELECT
user_id,
created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS rn
FROM posts;문법
윈도 함수는 GROUP BY 와 달리 원본 행을 유지한 채 계산 결과를 덧붙인다
GROUP BY 는 여러 행을 하나의 집계 행으로 줄입니다. 윈도 함수는 OVER (...) 절을 통해 특정 범위의 다른 행들을 참조하면서도 각 행을 그대로 유지합니다. 이 차이가 핵심입니다. "사용자별 최신 글을 한 행씩 가져오되 게시글 원본 컬럼도 함께 보고 싶다"는 요구는 GROUP BY 로 표현하기 어렵고 윈도 함수로 자연스럽게 해결됩니다. 윈도 함수는 SELECT 와 ORDER BY 절에서만 쓸 수 있으며, WHERE 절에서는 직접 사용할 수 없습니다.
-- 사용자별 최신 글 1개 추출 (CTE + 윈도 함수 패턴)
WITH ranked AS (
SELECT
id, user_id, title, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS rn
FROM posts
)
SELECT id, user_id, title, created_at
FROM ranked
WHERE rn = 1;윈도 함수는 "행은 유지하고 계산만 덧붙인다"로 읽으면 된다
집계는 여러 행을 하나로 줄이고, 윈도 함수는 행 수를 유지한 채 순위나 누적값을 덧붙입니다. 그래서 "원본 행도 보고 싶고, 그 안에서 순위도 계산하고 싶다"면 윈도 함수가 더 자연스럽습니다.
SELECT
user_id,
created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS rn
FROM posts;PARTITION BY 는 그룹 경계를 정하고 ORDER BY 는 그룹 내 계산 순서를 정한다
PARTITION BY 는 GROUP BY 처럼 계산 범위를 나누지만 행을 줄이지 않습니다. 같은 PARTITION BY 값을 가진 행들이 하나의 "윈도"를 이룹니다. ORDER BY 는 윈도 안에서의 정렬 기준을 정하며, ROW_NUMBER, RANK, DENSE_RANK 같은 순위 함수는 이 순서를 기반으로 값을 계산합니다. ORDER BY 없이 SUM, AVG 를 쓰면 파티션 전체의 합계/평균이 모든 행에 동일하게 붙습니다.
-- PARTITION BY 없이 전체 기준 계산
SELECT
id,
amount,
SUM(amount) OVER () AS total_amount,
amount / SUM(amount) OVER () AS ratio
FROM payments;
-- PARTITION BY + ORDER BY: 사용자별 누적 합계
SELECT
user_id,
amount,
SUM(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS running_total
FROM payments;GROUP BY와 윈도 함수는 "행을 줄일지 유지할지"로 먼저 구분하면 된다
결과를 사용자당 한 줄로 줄이고 싶으면 GROUP BY, 원본 행을 유지한 채 순위나 누적값을 붙이고 싶으면 윈도 함수가 맞습니다. 두 패턴은 비슷한 집계 함수를 써도 결과 행 수 자체가 다르므로, "최종 결과가 몇 줄이어야 하는가"를 먼저 정하면 선택이 쉬워집니다.
-- 사용자당 한 줄
SELECT user_id, COUNT(*) AS post_count
FROM posts
GROUP BY user_id;
-- 원본 행 유지
SELECT user_id, id, COUNT(*) OVER (PARTITION BY user_id) AS post_count
FROM posts;ROW_NUMBER / RANK / DENSE_RANK 는 동일 순위 처리 방식이 다르다
세 함수는 모두 ORDER BY 기준 순위를 매기지만 동점 처리가 다릅니다. ROW_NUMBER 는 동점이어도 임의로 다른 번호를 부여해 항상 연속된 유일 번호를 만듭니다. RANK 는 동점에 같은 순위를 주고 다음 순위를 건너뜁니다(1, 1, 3). DENSE_RANK 는 동점에 같은 순위를 주되 다음 순위를 건너뛰지 않습니다(1, 1, 2). "N등까지만 선택" 기준에 따라 어느 함수를 쓸지 달라집니다.
SELECT
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM quiz_results;윈도 함수는 SELECT 이후에 평가되므로 WHERE / HAVING 에서 결과를 바로 참조할 수 없다
윈도 함수는 WHERE, GROUP BY, HAVING 이 모두 처리된 뒤 SELECT 단계에서 계산됩니다. 따라서 WHERE rn = 1 처럼 윈도 함수 결과로 행을 필터링하려면 서브쿼리나 CTE 로 한 번 감싸야 합니다. 이 구조적 제약을 이해하면 "윈도 함수 결과를 조건으로 쓰고 싶은데 안 된다"는 오류 상황을 바로 파악할 수 있습니다.
-- 잘못된 패턴: WHERE 절에서 윈도 함수 직접 참조 불가
-- SELECT id, ROW_NUMBER() OVER (...) AS rn FROM posts WHERE rn = 1; -- 오류
-- 올바른 패턴: CTE 로 한 단계 감싸기
WITH numbered AS (
SELECT
id, user_id, title,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM posts
)
SELECT id, user_id, title
FROM numbered
WHERE rn = 1;선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 행을 줄이지 않고 그룹별 계산 결과가 필요할 때 | 윈도 함수 + OVER (PARTITION BY ...) |
| 그룹당 상위 N 개 행 추출 | ROW_NUMBER() OVER (...) + CTE 필터 |
| 동점 처리가 필요한 순위 | RANK (건너뜀) 또는 DENSE_RANK (연속) |
| 그룹 내 누적 합계 / 이동 평균 | SUM / AVG OVER (PARTITION BY ... ORDER BY ...) |
| 윈도 함수 결과로 행 필터링 | CTE 또는 서브쿼리로 한 단계 감싸기 |
주의할 점
윈도 함수는 WHERE, HAVING 절에서 직접 참조할 수 없습니다. 윈도 함수 결과를 필터 조건으로 쓰려면 반드시 CTE 나 서브쿼리로 한 단계 감싸야 합니다.
또한 윈도 함수는 집계와 달리 원본 행 수를 줄이지 않으므로 "사용자당 한 줄만 필요하다"면 GROUP BY 또는 ROW_NUMBER + WHERE rn = 1 패턴을 써야 합니다.
SELECT
user_id,
COUNT(*) OVER (PARTITION BY user_id) AS post_count
FROM posts;이 쿼리는 사용자별 개수를 계산하지만, 행 수는 그대로 유지됩니다. "사용자당 한 줄" 결과를 기대하면 GROUP BY와 헷갈린 상태일 가능성이 큽니다.
SELECT
user_id,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id) AS rn
FROM posts;ROW_NUMBER에 ORDER BY를 빼면 각 파티션 안에서 어떤 행이 1번이 되는지 안정적으로 보장되지 않습니다. 순위나 "최신 1개" 같은 문제는 항상 정렬 기준을 함께 둬야 합니다.
참고 링크
2 sources