Skip to Content
자격증SQLD20. 윈도우 함수 2: 집계 함수와 프레임(ROWS/RANGE)

이번 문서의 목표: 이 파일을 다 읽으면 SUM() OVER가 왜 행마다 다른 값을 내는지, ROWS BETWEEN으로 지정하는 프레임이 계산 범위를 어떻게 정하는지, 그리고 LAST_VALUE가 기본 설정에서 왜 “마지막 값”을 돌려주지 않는지를 근거를 들어 설명할 수 있게 된다.

왜 집계 함수를 윈도우로 다시 쓰는가

15편에서 SUM, AVG 같은 그룹 함수를 GROUP BY와 함께 배웠습니다. 이 함수들은 여러 행의 값을 하나로 뭉쳐 그룹당 한 숫자를 냅니다. 그런데 19편에서 확인했듯, 윈도우 함수의 핵심은 행을 줄이지 않는 것입니다.

같은 SUM, AVG, COUNT, MAX, MINOVER 절과 함께 쓰면, 각 행 옆에 “이 행이 속한 범위에서의 합계·평균”을 원본 행 개수 그대로 붙일 수 있습니다. 예를 들어 “각 직원의 급여와 함께, 그 부서 전체 급여 합계를 나란히 보여달라”는 요청은 GROUP BY로는 불가능하지만 윈도우 함수로는 자연스럽게 됩니다.

쉽게 말하면: 그룹 함수를 OVER 절과 함께 쓰면, “그룹으로 묶어서 요약하되 원본 행은 하나도 지우지 마라”는 뜻이 됩니다.

1. 집계 윈도우 함수 — SUM, AVG를 행마다 보여주기

작은 예시 — 입력 테이블

부서별 급여 데이터입니다(단위: 백만 원).

사번이름부서급여
101김민수영업90
102이서연영업90
103박지훈영업85
104최유리영업80
105정하늘기획95
106한소율기획70

SQL

SELECT 사번, 이름, 부서, 급여, SUM(급여) OVER (PARTITION BY 부서) AS 부서합계, AVG(급여) OVER (PARTITION BY 부서) AS 부서평균 FROM 직원;

결과 테이블

사번이름부서급여부서합계부서평균
101김민수영업9034586.25
102이서연영업9034586.25
103박지훈영업8534586.25
104최유리영업8034586.25
105정하늘기획9516582.5
106한소율기획7016582.5

결과 해석: ORDER BY 없이 PARTITION BY만 쓰면 파티션(부서) 전체가 계산 범위가 되어, 같은 부서 행은 모두 같은 합계·평균 값을 공유합니다. “내 급여가 부서 평균보다 높은가”를 한 줄로 비교하고 싶을 때 이 형태가 그대로 쓰입니다.

2. 프레임 절 — 계산 범위를 더 좁게 지정하기

ORDER BY를 추가하면 이야기가 달라집니다. ORDER BY가 있으면 “각 행 기준으로 어디부터 어디까지를 더할 것인가”라는 질문이 생기고, 이 범위를 정하는 것이 프레임(frame, 틀·범위라는 뜻) 절입니다.

쉽게 말하면: 프레임은 “지금 이 행을 계산할 때, 파티션 안의 몇 번째 행부터 몇 번째 행까지를 재료로 쓸 것인가”를 정하는 범위 지정자입니다.

프레임 문법

함수(컬럼) OVER ( PARTITION BY 컬럼 ORDER BY 컬럼 ROWS BETWEEN 시작 AND )

경계로 쓸 수 있는 값은 다음과 같습니다.

경계 표현의미
UNBOUNDED PRECEDING파티션의 맨 처음 행부터
N PRECEDING현재 행 기준 N행 앞부터
CURRENT ROW현재 행까지(또는부터)
N FOLLOWING현재 행 기준 N행 뒤까지
UNBOUNDED FOLLOWING파티션의 맨 마지막 행까지

PRECEDING은 “앞선, 선행하는”이라는 뜻이고 FOLLOWING은 “뒤따르는”이라는 뜻입니다. UNBOUNDED는 “경계가 없는”이라는 뜻으로, 파티션의 끝까지 무한히 확장한다는 의미입니다.

ROWS와 RANGE의 차이

프레임을 지정하는 방식에는 ROWSRANGE 두 가지가 있습니다.

  • ROWS: 물리적인 행 개수로 범위를 정합니다. “앞 1행부터 뒤 1행까지”처럼 정렬 순서상 몇 번째 행인지만 봅니다.
  • RANGE: 정렬 기준 컬럼의 값 범위로 범위를 정합니다. 예를 들어 RANGE BETWEEN 500 PRECEDING AND 500 FOLLOWING은 행 개수가 아니라 “현재 값보다 500 작은 값부터 500 큰 값까지”를 범위로 봅니다. 정렬 기준 값이 동점인 행이 여러 개 있으면 ROWSRANGE의 결과가 달라질 수 있습니다.

자주 틀리는 점: ROWSRANGE를 같은 것으로 착각합니다. ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING은 항상 최대 3개의 물리적 행(앞 1, 현재, 뒤 1)을 더합니다. 반면 RANGE BETWEEN 500 PRECEDING AND 500 FOLLOWING은 정렬 기준 컬럼 값이 현재 값과 500 이내 차이인 행을 전부 포함하므로, 그 범위에 몇 개의 행이 들어가는지는 데이터 값에 따라 달라집니다.

작은 예시 — ROWS로 이동 합계 계산하기

주문 4건이 있습니다.

ID주문일주문액
11월 3일100
21월 5일200
31월 8일300
41월 12일400

SQL

SELECT ID, 주문일, 주문액, SUM(주문액) OVER ( ORDER BY 주문일 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS 이동합계 FROM 주문;

결과 테이블

ID주문일주문액이동합계
11월 3일100300
21월 5일200600
31월 8일300900
41월 12일400700

결과 해석: 각 행의 이동합계는 “바로 앞 1행 + 현재 행 + 바로 뒤 1행”입니다. ID 1은 앞 행이 없어 100+200=300, ID 2는 100+200+300=600, ID 3은 200+300+400=900, ID 4는 뒤 행이 없어 300+400=700입니다. 프레임 경계 밖의 행은 존재하지 않는 것처럼 자동으로 제외되기 때문에 첫 행과 마지막 행은 합산 대상이 자연히 줄어듭니다.

프레임을 생략하면 어떻게 되는가

ORDER BY만 쓰고 프레임을 아예 생략하면, DBMS는 기본 프레임을 자동으로 적용합니다. 그 기본값이 바로 다음 절의 핵심 함정입니다.

ORDER BY만 쓰고 프레임 생략 시 기본값: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

즉 “파티션의 맨 처음부터 현재 행까지”가 기본 범위입니다. 그래서 ORDER BY만 있고 프레임이 없는 SUM() OVER는 흔히 누적 합계(맨 처음부터 지금까지 계속 더한 값)로 동작합니다.

3. LAG와 LEAD — 이전 행, 다음 행 값 가져오기

LAG(래그, “뒤처지다·지연되다”라는 뜻)와 LEAD(리드, “앞서다·이끌다”라는 뜻)는 현재 행을 기준으로 정렬 순서상 이전 행 또는 다음 행의 값을 그대로 가져오는 함수입니다. 전월 대비, 전일 대비 같은 비교 계산에 필수입니다.

LAG(컬럼, 오프셋, 기본값) OVER (ORDER BY 정렬컬럼) LEAD(컬럼, 오프셋, 기본값) OVER (ORDER BY 정렬컬럼)
  • 오프셋: 몇 행 앞(또는 뒤)을 볼지. 생략하면 1행입니다.
  • 기본값: 참조할 행이 없을 때(맨 첫 행에서 LAG를 쓰는 경우 등) 대신 넣을 값. 생략하면 NULL입니다.

작은 예시 — 입력 테이블

이름급여
최수진70
박민재85
이영희90
김철수100

SQL — 직전 급여와 비교하기

SELECT 이름, 급여, LAG(급여) OVER (ORDER BY 급여) AS 직전급여 FROM 성적;

결과 테이블

이름급여직전급여
최수진70NULL
박민재8570
이영희9085
김철수10090

결과 해석: ORDER BY 급여(오름차순)로 정렬했으므로 “이전 행”은 곧 급여가 한 단계 낮은 행입니다. 첫 행인 최수진은 이전 행이 없어 NULL이 나옵니다.

자주 틀리는 점: LAG의 정렬 방향을 DESC로 바꾸면 “이전 행”의 의미 자체가 뒤집힙니다. ORDER BY 급여 DESC에서 LAG(급여)를 쓰면 정렬상 바로 앞은 급여가 더 큰 행이 되어, 의도했던 “직전(더 작은 쪽) 급여”와 반대 결과가 나옵니다. 또한 LAGLEAD를 서로 반대로 착각하는 문제도 자주 나옵니다. LAG는 뒤(이전)를 보고, LEAD는 앞(다음)을 봅니다 — “리드보컬이 앞장서 이끈다”처럼 LEAD가 앞선다고 연결해 외우면 헷갈리지 않습니다.

4. FIRST_VALUE와 LAST_VALUE — 프레임의 첫 값, 마지막 값

FIRST_VALUELAST_VALUE는 이름 그대로 현재 프레임 안에서 정렬 기준으로 첫 번째, 마지막 값을 가져옵니다. 여기서 “현재 프레임 안에서”라는 조건이 결정적입니다.

FIRST_VALUE는 대체로 안전하다

SELECT 이름, 급여, FIRST_VALUE(급여) OVER (ORDER BY 급여) AS 최소급여 FROM 성적;

ORDER BY 급여(오름차순)로 정렬하면 프레임의 시작은 항상 파티션의 첫 행(가장 작은 값)입니다. 프레임을 생략해도 기본 프레임인 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW가 이미 “맨 처음부터”를 포함하므로, FIRST_VALUE는 어느 행에서 보든 항상 같은 최소값 70을 반환합니다. 즉 기본 프레임에서도 FIRST_VALUE는 기대한 대로 동작합니다.

LAST_VALUE가 함정인 이유

문제는 LAST_VALUE입니다. “마지막 값이니까 당연히 파티션의 최댓값(또는 정렬상 끝값)이 나오겠지”라고 기대하기 쉽지만, 기본 프레임에서는 그렇지 않습니다.

SELECT 이름, 급여, LAST_VALUE(급여) OVER (ORDER BY 급여) AS 최대급여_기대 FROM 성적;
이름급여최대급여_기대
최수진7070
박민재8585
이영희9090
김철수100100

결과 해석: 100(전체 최댓값)이 나올 것이라 기대했지만, 실제로는 각 행 자신의 급여값이 그대로 나옵니다. 이유는 기본 프레임이 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, 즉 “맨 처음부터 현재 행까지”이기 때문입니다. LAST_VALUE는 “이 프레임 안에서 마지막 값”을 찾는데, 프레임의 끝이 바로 CURRENT ROW(현재 행)로 고정되어 있으니, 결국 그 프레임의 마지막 값은 언제나 현재 행 자신이 되어버립니다.

비유: FIRST_VALUE는 “지금까지 읽은 페이지 중 첫 페이지가 몇 쪽인가”를 묻는 것과 같아서 항상 1쪽으로 답이 고정됩니다. LAST_VALUE는 “지금까지 읽은 페이지 중 마지막 페이지가 몇 쪽인가”를 묻는 것인데, “지금까지”의 끝은 항상 방금 읽은 그 페이지이므로 매번 다른 답(현재 페이지 번호)이 나오는 것뿐입니다. 책 전체의 마지막 페이지를 물은 게 아니었던 겁니다.

올바른 사용법 — 프레임을 파티션 전체로 넓히기

LAST_VALUE로 진짜 파티션의 마지막 값(또는 최댓값)을 얻으려면 프레임을 명시적으로 끝까지 넓혀야 합니다.

SELECT 이름, 급여, LAST_VALUE(급여) OVER ( ORDER BY 급여 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS 최대급여 FROM 성적;
이름급여최대급여
최수진70100
박민재85100
이영희90100
김철수100100

결과 해석: 프레임을 UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(파티션 전체)으로 넓히자 비로소 모든 행에서 파티션 전체의 마지막 값인 100이 반환됩니다. 실무에서는 최댓값이 필요하면 아예 MAX() OVER (PARTITION BY ...)를 쓰는 편이 프레임을 신경 쓸 필요가 없어 더 안전합니다.

자주 틀리는 점 (대표 함정): LAST_VALUE가 항상 파티션의 마지막(최댓값)을 반환한다고 착각하는 것이 SQLD에서 가장 자주 나오는 함정 중 하나입니다. ORDER BY만 쓰고 프레임을 생략하면 기본 프레임이 ... AND CURRENT ROW로 끝나기 때문에, LAST_VALUE는 사실상 “현재 행 자신의 값”을 돌려주는 것과 같아집니다. 이 문제를 피하려면 ① 프레임을 UNBOUNDED FOLLOWING까지 명시적으로 넓히거나, ②애초에 MAX() OVER를 쓰는 두 가지 방법 중 하나를 선택해야 합니다.

직접 해보기

00편에서 만든 실습 환경에서 진행합니다. 아직 안 만들었다면 00편을 먼저 보세요. seed.sql의 성적 테이블(이름, 급여 컬럼)을 그대로 씁니다.

프레임 절이 없을 때와 명시했을 때 누적 합계가 정말 같은 값인지 나란히 조회해 확인한다.

SELECT 이름, 급여, SUM(급여) OVER (ORDER BY 급여) AS 누적합계_기본프레임, SUM(급여) OVER ( ORDER BY 급여 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS 누적합계_명시프레임 FROM 성적;

무엇을 보아야 하나: 두 컬럼 값이 실제로 행마다 똑같이 나오는지 확인한다. 기본 프레임 자체가 이 명시적 프레임과 같다는 뜻이다.

직접 해보기 2

이 편 최대 함정인 LAST_VALUE를 직접 실행해 눈으로 확인한다. 프레임 없이 한 번, 파티션 전체로 넓혀서 한 번 조회한다.

SELECT 이름, 급여, LAST_VALUE(급여) OVER (ORDER BY 급여) AS 최대급여_함정, LAST_VALUE(급여) OVER ( ORDER BY 급여 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS 최대급여_정상 FROM 성적;

무엇을 보아야 하나: 첫 번째 컬럼(최대급여_함정)이 100이 아니라 매 행 자신의 급여값을 그대로 돌려주는지, 두 번째 컬럼(최대급여_정상)은 모든 행에서 100으로 똑같이 나오는지 확인한다.

왜 이걸 해보나: “LAST_VALUE는 항상 마지막(최댓값)을 돌려준다”는 착각이 이 편 기출의 대표 함정이다. 두 결과를 나란히 보면 프레임의 끝이 CURRENT ROW로 고정된다는 것이 무슨 뜻인지 확실해진다.

핵심 정리

  • 집계 함수를 OVER와 함께 쓰면 행을 줄이지 않고 그룹 단위 요약값을 각 행에 붙일 수 있다.
  • 프레임은 ROWS BETWEEN 시작 AND 끝(또는 RANGE)으로 지정하며, ROWS는 물리적 행 개수, RANGE는 정렬 기준 값의 범위로 계산 범위를 정한다.
  • ORDER BY만 쓰고 프레임을 생략하면 기본값은 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW이며, 이 때문에 집계 함수는 흔히 누적 합계로 동작한다.
  • LAG는 이전 행, LEAD는 다음 행의 값을 가져온다. 정렬 방향을 바꾸면 “이전·다음”의 의미도 뒤집힌다.
  • FIRST_VALUE는 기본 프레임에서도 안전하게 동작하지만, LAST_VALUE는 기본 프레임의 끝이 CURRENT ROW로 고정되어 있어 매 행마다 자기 자신의 값을 반환하는 함정에 빠지기 쉽다. 진짜 마지막 값이 필요하면 프레임을 UNBOUNDED FOLLOWING까지 넓히거나 MAX() OVER를 쓴다.

마무리 복습

문제 14지선다
ORDER BY만 사용하고 프레임 절을 생략했을 때 적용되는 기본 프레임은?
문제 24지선다
다음 중 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING의 의미로 옳은 것은?
문제 34지선다
ROWS와 RANGE의 근본적인 차이는?
문제 44지선다
LAG(급여) OVER (ORDER BY 급여)를 실행했을 때 첫 번째(가장 급여가 낮은) 행의 결과값은?
문제 54지선다
LAST_VALUE(급여) OVER (ORDER BY 급여)를 프레임 없이 실행하면 각 행에서 어떤 값이 나오는가?
문제 64지선다
LAST_VALUE로 파티션 전체의 진짜 마지막 값을 얻으려면 어떻게 해야 하는가?
문제 74지선다
SUM(급여) OVER (PARTITION BY 부서)를 ORDER BY 없이 사용했을 때의 동작은?
문제 84지선다
LAG와 LEAD의 차이를 올바르게 설명한 것은?

참고 자료

Last updated on