문제 상황: 각 그룹 안에서만 따로 순위를 매겨 원하는 데이터 뽑기
"각 부서별로 연봉이 가장 높은 2명만 뽑아오세요."
"게시판 종류별로 가장 최근에 작성된 게시글 5개씩만 가져오세요."
실무에서 정말 흔하게 마주치는 요구사항입니다. 전체 데이터에서 1등부터 N등까지 뽑는 건 TOP N
으로 간단하지만, '그룹별로' 라는 조건이 붙으면 복잡해집니다. 이럴 때 사용하는 가장 강력하고 표준적인 방법이 바로 윈도우 함수(Window Function)인 ROW_NUMBER()
입니다.
ROW_NUMBER()
와 PARTITION BY
를 함께 사용하면 그룹별로 순번을 매길 수 있습니다. 아래 코드는 각 부서(dept_name)별로 연봉(salary)이 높은 순서대로 순위를 매기는 가장 기본적인 템플릿입니다.
-- CTE(Common Table Expression)를 사용하여 순위가 매겨진 가상 테이블을 생성
WITH RankedEmployees AS (
SELECT
emp_name,
dept_name,
salary,
ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rn
FROM Employees
)
-- 위에서 생성한 가상 테이블에서 순위(rn)가 1~2위인 직원만 선택
SELECT *
FROM RankedEmployees
WHERE rn <= 2;
핵심 원리: ROW_NUMBER()와 PARTITION BY의 작동 방식
ROW_NUMBER()
는 이름 그대로 행의 번호를 매겨주는 함수입니다. 진짜 강력함은 OVER()
절과 함께 사용될 때 발휘됩니다.
ROW_NUMBER() OVER (PARTITION BY [그룹으로 묶을 컬럼] ORDER BY [순위를 매길 기준 컬럼])
PARTITION BY
: 이 키워드가 핵심입니다. 지정된 컬럼을 기준으로 데이터 그룹을 나눕니다. 마치 엑셀에서 '부분합'을 위해 그룹을 묶는 것과 같습니다.ORDER BY
: 나눠진 각 그룹 안에서 어떤 기준으로 순서를 정할지 결정합니다.- 동작 순서: 먼저
PARTITION BY
로 그룹을 나눈 뒤, 각 그룹 안에서ORDER BY
로 줄을 세우고, 맨 앞부터ROW_NUMBER()
가 1, 2, 3... 번호표를 나눠주는 그림을 상상하면 쉽습니다.
실무 활용 예제 3가지
이해를 돕기 위해 실제 업무에서 바로 쓸 수 있는 3가지 시나리오를 코드로 보여드리겠습니다.
예제 1: 부서별 연봉 Top 2 직원 찾기 (가장 기본)
-- 1. 테스트용 테이블 및 데이터 생성
CREATE TABLE Employees (
emp_name VARCHAR(50),
dept_name VARCHAR(50),
salary INT
);
INSERT INTO Employees VALUES
('김철수', '인사팀', 5000), ('이영희', '인사팀', 6200), ('박대리', '인사팀', 5500),
('최과장', '개발팀', 7000), ('정주임', '개발팀', 8500), ('윤사원', '개발팀', 6500),
('한차장', '영업팀', 9000), ('강부장', '영업팀', 8200);
-- 2. 그룹별 순위(rn)를 매긴 후, 1~2위만 조회
WITH RankedEmployees AS (
SELECT
emp_name, dept_name, salary,
ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rn
FROM Employees
)
SELECT * FROM RankedEmployees WHERE rn <= 2;
-- 3. 테스트 테이블 삭제
DROP TABLE Employees;
예제 2: 게시판별 최신 글 3개씩 가져오기
ORDER BY
에 날짜/시간 컬럼을 DESC
(내림차순)로 지정하면 '최신 순'으로 순위를 매길 수 있습니다.
-- 1. 테스트용 테이블 및 데이터 생성
CREATE TABLE Articles (
article_id INT,
board_name VARCHAR(50),
title VARCHAR(100),
created_at DATETIME
);
INSERT INTO Articles VALUES
(1, '공지', '서버 점검 안내', '2025-08-15 10:00:00'),
(2, '공지', '업데이트 안내', '2025-08-14 11:00:00'),
(3, '자유', '오늘 날씨 좋네요', '2025-08-15 13:00:00'),
(4, '자유', '다들 맛점하세요', '2025-08-15 12:00:00'),
(5, '자유', '반갑습니다', '2025-08-13 18:00:00'),
(6, '자유', '주말 계획 있으세요?', '2025-08-15 09:00:00');
-- 2. 게시판별 최신순(rn)을 매긴 후, 1~3위만 조회
WITH LatestArticles AS (
SELECT
board_name, title, created_at,
ROW_NUMBER() OVER (PARTITION BY board_name ORDER BY created_at DESC) AS rn
FROM Articles
)
SELECT * FROM LatestArticles WHERE rn <= 3;
-- 3. 테스트 테이블 삭제
DROP TABLE Articles;
예제 3: 중복 데이터 제거하고 최신 데이터만 남기기
데이터 클리닝 시 매우 유용한 고급 기술입니다. ROW_NUMBER()
를 사용해 중복된 데이터 중 최신 데이터에만 1번을 부여하고, 나머지를 삭제하는 방식입니다.
-- 1. 중복 데이터가 포함된 테이블 생성
CREATE TABLE CustomerLogs (
email VARCHAR(100),
status VARCHAR(10),
last_updated DATETIME
);
INSERT INTO CustomerLogs VALUES
('a@test.com', '활성', '2025-08-10'), ('a@test.com', '탈퇴', '2025-08-12'),
('b@test.com', '활성', '2025-08-11'),
('c@test.com', '휴면', '2025-08-09'), ('c@test.com', '활성', '2025-08-14');
-- 2. 이메일별로 최신 데이터에만 1번을 부여하고, 1번이 아닌(오래된) 데이터 삭제
WITH DuplicatedLogs AS (
SELECT
ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_updated DESC) AS rn
FROM CustomerLogs
)
DELETE FROM DuplicatedLogs WHERE rn > 1;
-- 3. 결과 확인
SELECT * FROM CustomerLogs;
-- 4. 테스트 테이블 삭제
DROP TABLE CustomerLogs;
잠깐! ROW_NUMBER() vs RANK(), DENSE_RANK()
비슷한 순위 함수로 RANK()
와 DENSE_RANK()
가 있습니다. 공동 순위를 어떻게 처리하는지가 다릅니다.
함수 | 예시 (점수: 100, 90, 90, 80) | 특징 |
ROW_NUMBER() |
1, 2, 3, 4 |
공동 순위가 있어도 무조건 고유한 순번을 매김 |
RANK() |
1, 2, 2, 4 |
공동 순위 다음은 그만큼 건너뛰고 매김 |
DENSE_RANK() |
1, 2, 2, 3 |
공동 순위가 있어도 건너뛰지 않고 촘촘하게 매김 |
'Top N'을 뽑을 때는 중복 없이 고유한 순번을 매기는 ROW_NUMBER()
가 가장 명확하고 자주 사용됩니다.
최종 요약
- 그룹별 순위를 매길 땐
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)
를 사용하세요. - 결과를 필터링하려면 반드시 CTE(
WITH ... AS
)나 서브쿼리로 감싸야 합니다. - 단순히 'Top N'개를 뽑을 땐
ROW_NUMBER()
가, '동점자를 모두 포함한 3위까지'를 뽑을 땐RANK()
나DENSE_RANK()
가 적합합니다.
'프로그래밍 > 개발자의 SQL 연구소' 카테고리의 다른 글
[MSSQL] DATEADD, DATEDIFF 완벽 정복: 날짜 계산 치트 시트 (1) | 2025.08.20 |
---|---|
[MSSQL] 문자열 분리: STRING\_SPLIT 기본기와 주의점 총정리 (0) | 2025.08.19 |
[MSSQL] CASE WHEN으로 똑똑하게 데이터 집계하기 (매출 구간별 분석) (1) | 2025.08.18 |
[MSSQL] ISNULL vs COALESCE, 차이점과 올바른 사용법 총정리 (3) | 2025.08.17 |
[MSSQL] 날짜 포맷 YYYY-MM-DD, 10초 해결과 성능 분석 핵심 가이드 (4) | 2025.08.15 |