인라인 뷰 대신 WITH 로 이름을 붙이면 안쪽 쿼리를 위에서 읽을 수 있어 길어질수록 편합니다. 여기서는 동점을 함께 살리는 RANK 를 씁니다.
WITH ranked AS (
SELECT
dept
, name
, sal
, RANK() OVER (PARTITION BY dept ORDER BY sal DESC) AS rnk
FROM emp
)
SELECT
dept
, name
, sal
, rnk
FROM ranked
WHERE rnk <= 2
ORDER BY dept, rnk, name;DEPT | NAME | SAL | RNK
-----+--------+------+----
개발 | 이부장 | 800 | 1
개발 | 박과장 | 700 | 2
개발 | 최대리 | 700 | 2
경영 | 김대표 | 1000 | 1
영업 | 정사원 | 600 | 1
영업 | 조사원 | 600 | 1
(6행)ROW_NUMBER 로 뽑은 5행과 달리 개발에서 700 동점인 최대리가 함께 나와 6행입니다. 영업은 공동 1등 두 명만 나오고 3등(윤사원)은 빠집니다.
같은 "상위 2명" 이라도 어떤 함수를 쓰느냐에 따라 결과 행 수가 달라집니다.
SELECT
t.dept
, t.name
, t.sal
, t.rn
, t.rnk
FROM (
SELECT
dept
, name
, sal
, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY sal DESC, id) AS rn
, RANK() OVER (PARTITION BY dept ORDER BY sal DESC) AS rnk
FROM emp
) t
WHERE t.rn <= 2 OR t.rnk <= 2
ORDER BY t.dept, t.rn;DEPT | NAME | SAL | RN | RNK
-----+--------+------+----+----
개발 | 이부장 | 800 | 1 | 1
개발 | 박과장 | 700 | 2 | 2
개발 | 최대리 | 700 | 3 | 2
경영 | 김대표 | 1000 | 1 | 1
영업 | 정사원 | 600 | 1 | 1
영업 | 조사원 | 600 | 2 | 1
(6행)rn <= 2 는 개발 2명·영업 2명이고, rnk <= 2 는 개발 3명·영업 2명입니다. 최대리는 rn 이 3 이라 ROW_NUMBER 기준에서는 빠지고 RANK 기준에서는 남습니다.
팁"정확히 N명" 이 필요하면
ROW_NUMBER(고유 컬럼으로 동점 고정), "동점은 모두 포함"이면RANK를 씁니다. 요구사항에 동점 처리가 없으면 먼저 확인합니다. Oracle 12c 이상에는FETCH FIRST n ROWS WITH TIES도 있지만 여기서는 언급만 합니다.
분석 함수 문법은 네 DB 가 같습니다. 다른 것은 인라인 뷰(파생 테이블) 별칭 규칙입니다. Oracle·Tibero 는 별칭 앞에 AS 를 쓰면 오류이고, MySQL·MSSQL 은 별칭이 필수입니다. 소스: 03_dialects.sql.
-- Oracle · Tibero
SET MODE Oracle;
SELECT
t.dept
, t.name
, t.sal
, t.rn
FROM (
SELECT
dept
, name
, sal
, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY sal DESC, id) AS rn
FROM emp
) t
WHERE t.rn <= 2
ORDER BY t.dept, t.rn;DEPT | NAME | SAL | RN
-----+--------+------+---
개발 | 이부장 | 800 | 1
개발 | 박과장 | 700 | 2
경영 | 김대표 | 1000 | 1
영업 | 정사원 | 600 | 1
영업 | 조사원 | 600 | 2
(5행)MySQL 8.0 과 MSSQL 2005 이상은 같은 문장에서 ) AS t 로 쓰거나 ) t 로 써도 됩니다. 별칭을 빼면 두 DB 모두 오류입니다. SET MODE MySQL·SET MODE MSSQLServer 로 실행한 결과도 위와 같은 5행이었습니다.
주의H2 는 Oracle 모드에서도
) AS t를 허용하고 별칭 없는 파생 테이블도 통과시킵니다. 이 차이는 H2 에서 재현 안 됨, 문법 검토만 하세요. MySQL 5.7 이하에서는 분석 함수 자체가 없으므로 이 문장이 실행되지 않습니다.
로그 테이블에서 사용자별 가장 최근 로그인 1건만 남기는 패턴입니다. PARTITION BY 에 중복을 판단하는 키를, ORDER BY 에 "남기고 싶은 순서"를 두고 rn = 1 만 고릅니다.
SELECT
t.id
, t.user_id
, t.login_date
FROM (
SELECT
id
, user_id
, login_date
, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date DESC, id DESC) AS rn
FROM login_log
) t
WHERE t.rn = 1
ORDER BY t.user_id;ID | USER_ID | LOGIN_DATE
---+---------+-----------
6 | kim | 2026-09-09
5 | lee | 2026-09-07
4 | park | 2026-09-03
(3행)같은 날짜가 여럿일 때를 대비해 id DESC 를 덧붙여 결과를 고정했습니다. 이 패턴은 조회뿐 아니라 중복 행을 지우는 DELETE 의 대상 선정에도 그대로 쓰입니다.