이번 문서의 목표: 이 파일을 다 읽으면 PARTITION BY와 GROUP BY를 헷갈리지 않고, 동점이 섞인 데이터에서 RANK·DENSE_RANK·ROW_NUMBER가 각각 어떤 순위를 매기는지 표를 안 봐도 계산할 수 있으며, NTILE로 데이터를 몇 개 그룹으로 나눌 때 결과를 예측할 수 있게 된다.
왜 윈도우 함수가 따로 필요한가
15편에서 GROUP BY와 HAVING을 배웠습니다. GROUP BY는 여러 행을 하나로 묶어서 그룹당 하나의 요약값(합계, 평균, 개수)만 남기는 도구입니다. 그런데 실무에서는 이런 요구가 자주 생깁니다.
“각 부서에서 급여가 몇 등인지, 원래 직원 목록은 그대로 유지한 채 옆에 순위 컬럼만 하나 추가하고 싶다.”
GROUP BY로는 이걸 할 수 없습니다. GROUP BY 부서를 쓰는 순간 부서별로 여러 행이 있던 직원 데이터가 부서 하나당 한 행으로 뭉개지기 때문입니다. 개별 직원의 급여, 이름 같은 상세 정보는 사라집니다.
이 문제를 풀기 위해 SQL 표준은 윈도우 함수(window function, 분석 함수라고도 부릅니다. analytic function)를 도입했습니다.
쉽게 말하면:
GROUP BY는 여러 행을 한 행으로 뭉치는 도구이고, 윈도우 함수는 행 개수를 그대로 둔 채 각 행 옆에 “이 행이 속한 그룹에서는 이런 값이다”라는 곁가지 정보를 붙여주는 도구입니다.
1. 윈도우 함수의 구조 — OVER, PARTITION BY, ORDER BY
기본 골격
윈도우 함수는 일반 함수 뒤에 OVER 절을 붙인 형태로 씁니다.
함수이름(인자) OVER (
[PARTITION BY 컬럼]
[ORDER BY 컬럼]
)OVER는 영어 그대로 “~에 걸쳐서”라는 뜻입니다. 이 함수의 계산 범위가 테이블 전체가 아니라 OVER 괄호 안에서 정한 특정 범위(윈도우, window — 창문이라는 뜻으로, 전체 데이터 중 이 함수가 내다보는 부분 구간을 창문에 빗댄 이름입니다)에 걸쳐 이루어진다는 뜻입니다.
OVER 괄호 안에는 두 가지 절이 들어갈 수 있습니다.
PARTITION BY: 계산을 어떤 단위로 나눌지 정합니다. “부서별로 따로 계산해라”는 뜻입니다. 생략하면 테이블 전체가 하나의 파티션(partition, 분할 구역이라는 뜻)이 됩니다.ORDER BY: 그 파티션 안에서 어떤 기준으로 줄을 세울지 정합니다. 순위 함수라면 이 순서가 곧 순위 기준이 됩니다.
작은 예시 — 입력 테이블
부서별 급여 데이터로 실제 동작을 확인하겠습니다.
| 사번 | 이름 | 부서 | 급여 |
|---|---|---|---|
| 101 | 김민수 | 영업 | 90 |
| 102 | 이서연 | 영업 | 90 |
| 103 | 박지훈 | 영업 | 85 |
| 104 | 최유리 | 영업 | 80 |
| 105 | 정하늘 | 기획 | 95 |
| 106 | 한소율 | 기획 | 70 |
(급여 단위는 백만 원입니다.)
SQL — 부서별 순위 매기기
SELECT 사번, 이름, 부서, 급여,
RANK() OVER (PARTITION BY 부서 ORDER BY 급여 DESC) AS 순위
FROM 직원;PARTITION BY 부서가 있으므로 영업 부서 4명과 기획 부서 2명은 서로 완전히 독립된 별개의 경쟁입니다. 영업 부서 안에서 1등이 나오는 것과 별개로, 기획 부서 안에서도 1등이 나옵니다. ORDER BY 급여 DESC는 급여가 높은 사람이 1등이 되도록 내림차순 정렬을 지정합니다.
결과 테이블
| 사번 | 이름 | 부서 | 급여 | 순위 |
|---|---|---|---|---|
| 101 | 김민수 | 영업 | 90 | 1 |
| 102 | 이서연 | 영업 | 90 | 1 |
| 103 | 박지훈 | 영업 | 85 | 3 |
| 104 | 최유리 | 영업 | 80 | 4 |
| 105 | 정하늘 | 기획 | 95 | 1 |
| 106 | 한소율 | 기획 | 70 | 2 |
결과 해석: 행이 6개에서 6개 그대로 유지되었습니다. GROUP BY였다면 부서별로 2행(영업, 기획)만 남았을 텐데, 윈도우 함수는 원본 행을 하나도 지우지 않고 옆에 순위라는 새 정보만 얹었습니다. 이것이 윈도우 함수의 존재 이유입니다.
자주 틀리는 점 — PARTITION BY와 GROUP BY를 같은 것으로 착각: GROUP BY는 결과 행 수를 그룹 개수로 줄이고, 그룹에 속하지 않은 컬럼(개별 직원 이름 등)은 SELECT에 쓸 수 없어 에러가 납니다. PARTITION BY는 행 수를 전혀 줄이지 않고, 원본 컬럼을 그대로 SELECT할 수 있습니다. 기출에서는 “이 쿼리의 실행 결과 행 수는?”을 물어 PARTITION BY를 GROUP BY로 착각했는지 확인하는 문제가 반복해서 나옵니다. 두 절이 함께 쓰인 것도 아니고, PARTITION BY가 있다고 해서 GROUP BY처럼 행이 줄지 않는다는 점을 반드시 기억해야 합니다.
2. 순위 함수 3종의 차이 — 동점을 다루는 방식
SQLD 시험에서 가장 자주 나오는 함정이 바로 이 세 함수의 차이입니다. 동점이 없는 데이터에서는 세 함수가 똑같이 동작합니다. 차이는 오직 동점이 있을 때만 드러납니다.
쉽게 말하면:
ROW_NUMBER는 동점이어도 무조건 순서대로 1, 2, 3, …을 매기는 “등수표 없는 번호표”이고,RANK는 동점에게 같은 등수를 주되 그다음 등수를 동점 인원 수만큼 건너뛰는 “육상 경기 시상대” 방식이며,DENSE_RANK는 동점에게 같은 등수를 주되 건너뛰지 않는 “촘촘한 등수” 방식입니다.
세 함수 정의
ROW_NUMBER(): 파티션 안에서 정렬 순서대로 고유한 일련번호를 1부터 매깁니다. 동점이어도 예외 없이 서로 다른 번호를 받습니다.RANK(): 동점인 행에는 같은 순위를 주고, 그다음 순위는 동점 인원 수만큼 건너뜁니다. 예를 들어 공동 1위가 두 명이면 다음 순위는 2가 아니라 3입니다.DENSE_RANK(): 동점인 행에는 같은 순위를 주지만, 그다음 순위는 건너뛰지 않고 바로 다음 정수를 씁니다. DENSE는 “촘촘한, 빽빽한”이라는 뜻으로, 순위 사이에 빈 번호가 생기지 않는다는 의미입니다.
작은 예시 — 동점이 포함된 입력 데이터
점수 90, 90, 85, 80을 가진 학생 4명으로 세 함수를 나란히 비교하겠습니다.
| 학생 | 점수 |
|---|---|
| 김철수 | 90 |
| 이영희 | 90 |
| 박민재 | 85 |
| 최수진 | 80 |
SQL — 세 함수를 한 쿼리에서 비교
SELECT 학생, 점수,
ROW_NUMBER() OVER (ORDER BY 점수 DESC) AS 행번호,
RANK() OVER (ORDER BY 점수 DESC) AS 순위,
DENSE_RANK() OVER (ORDER BY 점수 DESC) AS 조밀순위
FROM 성적;결과 테이블 — 세 함수를 한 표에 나란히
| 학생 | 점수 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| 김철수 | 90 | 1 | 1 | 1 |
| 이영희 | 90 | 2 | 1 | 1 |
| 박민재 | 85 | 3 | 3 | 2 |
| 최수진 | 80 | 4 | 4 | 3 |
결과 해석: 90점 동점자 두 명에서 세 함수가 갈립니다.
ROW_NUMBER는 동점이어도 1과 2로 다른 번호를 줍니다. 정렬 기준이 완전히 같은 값이라도 내부적으로는 행을 하나씩 구별해 번호를 매기기 때문입니다(어느 행이 1번이 될지는ORDER BY에 동점을 가를 추가 기준이 없으면 DBMS가 임의로 정합니다).RANK는 두 명 모두 1위를 주고, 그다음 3위인 박민재에게는 2를 건너뛰고 3을 줍니다. 공동 1위가 두 명이므로 “1등, 1등, 그다음은 3등”이라는 시상대식 계산입니다.DENSE_RANK는 두 명 모두 1위를 주는 것은 같지만, 박민재에게는 건너뛰지 않고 2위를 줍니다.
비유: 마라톤 대회에서 공동 1위가 두 명 나왔다고 생각해 보세요.
RANK는 “1등, 1등, 3등, 4등”처럼 시상대 자리 수 그대로 매깁니다(2등 자리가 빕니다).DENSE_RANK는 “1등, 1등, 2등, 3등”처럼 등수 종류만 셉니다(1등 종류, 2등 종류, 3등 종류— 총 3종류의 등수만 존재).ROW_NUMBER는 등수와 무관하게 그냥 도착한 순서대로 번호표를 나눠주는 접수처 직원입니다.
자주 틀리는 점: “상위 N등 안에 든 사람을 모두 뽑아라” 같은 문제에서 어떤 함수를 써야 하는지 헷갈립니다. 기준은 다음과 같습니다. 정확히 N행만 필요하면 ROW_NUMBER(동점을 억지로 갈라서라도 개수를 맞춤), N등이라는 등수 안에 동점자를 전부 포함하고 등수에 결번이 생겨도 상관없으면 RANK, 등수 종류로 N번째까지(결번 없이) 포함하려면 DENSE_RANK를 씁니다. 실제 기출에서도 “서로 다른 상위 3개 급여 수준을 모두 뽑으려면 어떤 함수를 써야 하는가”를 물었고, 정답은 DENSE_RANK였습니다. RANK를 쓰면 공동 순위 때문에 등수 결번이 생겨 원하는 수준 개수를 다 못 뽑을 수 있고, ROW_NUMBER는 동점인데도 임의로 순서를 갈라 일부만 뽑는 문제가 생깁니다.
3. NTILE — 데이터를 N개 그룹으로 나누기
NTILE(n)은 정렬된 행들을 n개의 그룹으로 최대한 균등하게 나눠 각 행에 그룹 번호(1부터 n까지)를 매기는 함수입니다. NTILE이라는 이름은 “N등분(tile)“에서 왔습니다. 성적을 4분위로 나누거나, 매출 순위를 상위·중위·하위 그룹으로 나눌 때 씁니다.
나머지가 있을 때의 배분 규칙
전체 행 수가 n으로 나누어떨어지지 않으면, 앞쪽 그룹부터 나머지를 한 행씩 더 배분합니다. 예를 들어 8행을 NTILE(3)으로 나누면 8을 3으로 나눈 몫 2, 나머지 2이므로 그룹 크기는 3, 3, 2가 됩니다. 즉 1번 그룹에 3행, 2번 그룹에 3행, 3번 그룹에 2행이 배정됩니다.
작은 예시 — 입력 테이블
급여 순으로 정렬한 직원 8명입니다(단위: 백만 원).
| 순번 | 이름 | 급여 |
|---|---|---|
| 1 | 직원A | 100 |
| 2 | 직원B | 95 |
| 3 | 직원C | 90 |
| 4 | 직원D | 85 |
| 5 | 직원E | 80 |
| 6 | 직원F | 75 |
| 7 | 직원G | 70 |
| 8 | 직원H | 65 |
SQL
SELECT 이름, 급여,
NTILE(3) OVER (ORDER BY 급여 DESC) AS 그룹
FROM 직원;결과 테이블
| 이름 | 급여 | 그룹 |
|---|---|---|
| 직원A | 100 | 1 |
| 직원B | 95 | 1 |
| 직원C | 90 | 1 |
| 직원D | 85 | 2 |
| 직원E | 80 | 2 |
| 직원F | 75 | 2 |
| 직원G | 70 | 3 |
| 직원H | 65 | 3 |
결과 해석: 8행을 3그룹으로 나눌 때 나머지 2가 앞쪽 그룹(1번, 2번)에 한 행씩 더 배분되어 3+3+2 크기가 되었습니다. NTILE을 1,2,3,1,2,3,...처럼 번갈아 매기는 라운드로빈 방식으로 착각하면 안 됩니다. NTILE은 정렬 순서대로 연속된 덩어리를 잘라 그룹을 만듭니다.
자주 틀리는 점: NTILE(n)의 그룹 번호를 라운드로빈(순서대로 1, 2, 3, 1, 2, 3, …)으로 매긴다고 착각하는 오답이 자주 나옵니다. 실제로는 정렬된 순서에서 앞부분을 통째로 1번 그룹, 그다음 부분을 2번 그룹으로 잘라내는 방식입니다. 나머지 배분도 “뒤쪽 그룹부터”가 아니라 반드시 앞쪽 그룹부터 한 행씩 더 준다는 점을 기억해야 합니다.
직접 해보기
00편에서 만든 실습 환경에서 진행합니다. 아직 안 만들었다면 00편을 먼저 보세요. seed.sql의 직원 테이블을 그대로 씁니다.
세 순위 함수를 한 쿼리에서 나란히 조회해, 동점(영업팀 급여 90)이 있는 지점에서 값이 어떻게 갈리는지 직접 본다.
SELECT 사번, 이름, 부서, 급여,
ROW_NUMBER() OVER (PARTITION BY 부서 ORDER BY 급여 DESC) AS 행번호,
RANK() OVER (PARTITION BY 부서 ORDER BY 급여 DESC) AS 순위,
DENSE_RANK() OVER (PARTITION BY 부서 ORDER BY 급여 DESC) AS 조밀순위
FROM 직원;무엇을 보아야 하나: 영업팀 급여 90인 두 행(김민수, 이서연)에서 세 컬럼 값이 서로 어떻게 다른지, 그다음 급여(85)의 순위·조밀순위가 서로 다른 숫자로 이어지는지 확인한다.
직접 해보기 2
이번엔 PARTITION BY를 빼고 같은 쿼리를 실행해, 순위가 부서 구분 없이 전체를 대상으로 매겨지는지 비교한다.
SELECT 사번, 이름, 부서, 급여,
RANK() OVER (ORDER BY 급여 DESC) AS 전체순위
FROM 직원;무엇을 보아야 하나: 기획팀 정하늘(급여 95)이 전체 1위로 올라오는지, 방금 전 PARTITION BY가 있던 결과와 순위 숫자가 어떻게 달라지는지 비교한다.
왜 이걸 해보나: 동점에서 RANK·DENSE_RANK가 갈리는 지점과 PARTITION BY 유무에 따른 순위 범위 차이는 이 편 기출에서 가장 자주 묻는 두 포인트다.
핵심 정리
- 윈도우 함수는
함수() OVER (PARTITION BY ... ORDER BY ...)구조로 쓰며, 행 개수를 줄이지 않고 각 행 옆에 집계·순위 정보를 덧붙인다. PARTITION BY는 계산 단위를 나누는 것이지 행을 뭉치는GROUP BY가 아니다. 이 둘을 구별하지 못하면 결과 행 수를 잘못 예측한다.- 동점이 없으면
ROW_NUMBER·RANK·DENSE_RANK는 동일하다. 차이는 동점에서만 드러난다. ROW_NUMBER는 동점이어도 고유 번호,RANK는 동점 뒤 순위를 건너뜀(결번 발생),DENSE_RANK는 동점 뒤에도 건너뛰지 않음(결번 없음).- “서로 다른 상위 N개 등급을 결번 없이 모두 뽑는” 상황에는
DENSE_RANK가 정답인 경우가 많다. NTILE(n)은 정렬된 행을 n개의 연속된 덩어리로 나누며, 나머지는 앞쪽 그룹부터 한 행씩 더 배분한다.