데이터 분석
[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
RANK·DENSE_RANK·ROW_NUMBER 한 번에 정리
RANK·DENSE_RANK·ROW_NUMBER 한 번에 정리

위그로스아카데미

채널별 ROAS 상위 캠페인을 뽑아달라는 요청 앞에서, SQL을 어느 정도 다루는 마케터도 멈추는 지점이 있습니다.
GROUP BY로 채널별 평균 ROAS는 계산할 수 있습니다. 그런데 각 채널 안에서 1위 캠페인 하나씩을 꺼내는 순간, GROUP BY는 한계에 부딪힙니다. 이 문제를 SQL 한 쿼리로 해결하는 것이 윈도우 함수(Window Function)입니다.
GROUP BY가 막히는 지점
GROUP BY는 데이터를 압축합니다. 채널별로 집계할 수 있지만, 그 과정에서 개별 캠페인 정보가 사라집니다.
-- 채널별 평균 ROAS — 가능하다 -- 그러나 '어떤 캠페인인지'는 알 수 없다 SELECT channel, AVG(roas) AS avg_roas FROM campaigns GROUP BY
결과로는 "검색광고 채널 평균 ROAS = 3.5"를 알 수 있습니다. 하지만 "검색광고 채널에서 ROAS가 가장 높은 캠페인이 무엇인지"는 알 수 없습니다. 집계되는 순간 개별 행이 사라지기 때문입니다.
이 지점에서 윈도우 함수가 필요해집니다.
윈도우 함수. 행을 유지하면서 계산을 추가한다
윈도우 함수는 GROUP BY와 달리 개별 행을 그대로 유지합니다. 원본 데이터를 살려두면서, 순위 컬럼 같은 계산 결과를 덧붙입니다.
-- 채널별 ROAS 순위를 컬럼으로 추가한다 SELECT campaign_id, channel, roas, RANK() OVER (PARTITION BY channel ORDER BY roas DESC) AS channel_rank FROM
OVER() 안의 구조가 핵심입니다.
함수명() OVER ( PARTITION BY 구역_기준 -- 어떤 단위로 나눌지 (채널별, 카테고리별 등) ORDER BY 정렬_기준 DESC -- 그 안에서 어떤 순서로 정렬할지 )
PARTITION BY: 데이터를 나누는 기준입니다.
PARTITION BY channel이면 채널별로 구역을 만듭니다. 생략하면 전체 테이블이 하나의 구역이 됩니다.ORDER BY: 구역 안에서의 정렬 기준입니다. 순위 함수에서는 필수입니다.
ROW_NUMBER, RANK, DENSE_RANK 동점 처리 방식
세 함수 모두 순위를 매기지만, 동점 처리 방식이 다릅니다. SQLD 2과목에서 거의 매 회차 출제되는 부분입니다.
campaign_id | channel | roas |
|---|---|---|
C001 | 검색광고 | 4.2 |
C002 | SNS | 3.8 |
C003 | 디스플레이 | 3.8 |
C004 | 검색광고 | 2.1 |
SELECT campaign_id, roas, ROW_NUMBER() OVER (ORDER BY roas DESC) AS row_num, RANK() OVER (ORDER BY roas DESC) AS rnk, DENSE_RANK() OVER (ORDER BY roas DESC) AS dense_rnk FROM
campaign_id | roas | row_num | rnk | dense_rnk |
|---|---|---|---|---|
C001 | 4.2 | 1 | 1 | 1 |
C002 | 3.8 | 2 | 2 | 2 |
C003 | 3.8 | 3 | 2 | 2 |
C004 | 2.1 | 4 | 4 | 3 |
C002와 C003이 ROAS 3.8로 동점입니다. 여기서 차이가 납니다.
ROW_NUMBER: 동점이어도 각 행에 다른 번호를 부여합니다. 정확히 N개만 뽑아야 할 때 씁니다.
RANK: 동점에 같은 순위를 주고, 다음 순위를 건너뜁니다. C002=C003=2등이므로 3등은 없고 C004=4등입니다.
DENSE_RANK: 동점에 같은 순위를 주되, 다음 순위를 건너뛰지 않습니다. C002=C003=2등, C004=3등입니다.
중요포인트: RANK는 gap이 생깁니다(3등이 없음). DENSE_RANK는 gap이 없습니다(3등이 있음). 이 차이 하나가 SQLD 선택지를 가릅니다.
실무 쿼리. 채널별 ROAS 1위 추출
각 채널에서 ROAS 1위 캠페인 하나씩 추출하는 쿼리입니다.
SELECT * FROM ( SELECT campaign_id, channel, roas, ROW_NUMBER() OVER ( PARTITION BY channel ORDER BY roas DESC ) AS rn FROM campaigns WHERE DATE_FORMAT(start_date, '%Y-%m') = '2024-10' ) ranked WHERE rn = 1
쿼리 실행 흐름: ├── FROM campaigns → 원본 테이블 전체 조회 ├── PARTITION BY channel → 채널별로 구역 생성 ├── ORDER BY roas DESC → 구역 내 ROAS 내림차순 정렬 ├── ROW_NUMBER() → rn → 각 구역의 1위에 rn=1 부여 └── WHERE rn = 1 → 채널별 1위만 최종 반환
RANK 대신 ROW_NUMBER를 선택한 이유가 있습니다. RANK를 쓰면 동점인 캠페인 두 개가 모두 rn=1이 됩니다. "채널당 1개씩"이라는 조건을 만족하지 못합니다. ROW_NUMBER는 동점이어도 하나에만 1번을 부여하기 때문에 정확히 1개씩 반환합니다.
한 가지 더 주의할 점이 있습니다. 윈도우 함수 결과(rn)는 같은 쿼리 안의 WHERE로 바로 필터링할 수 없습니다. 반드시 서브쿼리나 CTE로 감싼 뒤 바깥에서 필터링해야 합니다.
시험에 나오는 방식
SQLD 2과목에서 윈도우 함수 문제는 거의 매 회차 출제됩니다. 핵심 포인트를 정리합니다.
OVER()없이는 윈도우 함수가 아닙니다. GROUP BY와 혼동하지 않도록 주의합니다.PARTITION BY를 생략하면 전체 테이블이 하나의 구역으로 처리됩니다.
RANK는 gap 발생, DENSE_RANK는 gap 없음 — 선택지를 가르는 핵심입니다.
윈도우 함수 결과는 WHERE로 바로 필터링할 수 없습니다. 서브쿼리나 CTE가 필요합니다.
ROW_NUMBER는 동점이 있어도 반드시 고유한 번호를 부여합니다.
마치며
윈도우 함수의 핵심은 하나입니다. GROUP BY처럼 데이터를 압축하지 않고, 원본 행을 살린 채로 계산 결과를 추가한다는 것입니다. PARTITION BY로 구역을 나누고 ORDER BY로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.

채널별 ROAS 상위 캠페인을 뽑아달라는 요청 앞에서, SQL을 어느 정도 다루는 마케터도 멈추는 지점이 있습니다.
GROUP BY로 채널별 평균 ROAS는 계산할 수 있습니다. 그런데 각 채널 안에서 1위 캠페인 하나씩을 꺼내는 순간, GROUP BY는 한계에 부딪힙니다. 이 문제를 SQL 한 쿼리로 해결하는 것이 윈도우 함수(Window Function)입니다.
GROUP BY가 막히는 지점
GROUP BY는 데이터를 압축합니다. 채널별로 집계할 수 있지만, 그 과정에서 개별 캠페인 정보가 사라집니다.
-- 채널별 평균 ROAS — 가능하다 -- 그러나 '어떤 캠페인인지'는 알 수 없다 SELECT channel, AVG(roas) AS avg_roas FROM campaigns GROUP BY
결과로는 "검색광고 채널 평균 ROAS = 3.5"를 알 수 있습니다. 하지만 "검색광고 채널에서 ROAS가 가장 높은 캠페인이 무엇인지"는 알 수 없습니다. 집계되는 순간 개별 행이 사라지기 때문입니다.
이 지점에서 윈도우 함수가 필요해집니다.
윈도우 함수. 행을 유지하면서 계산을 추가한다
윈도우 함수는 GROUP BY와 달리 개별 행을 그대로 유지합니다. 원본 데이터를 살려두면서, 순위 컬럼 같은 계산 결과를 덧붙입니다.
-- 채널별 ROAS 순위를 컬럼으로 추가한다 SELECT campaign_id, channel, roas, RANK() OVER (PARTITION BY channel ORDER BY roas DESC) AS channel_rank FROM
OVER() 안의 구조가 핵심입니다.
함수명() OVER ( PARTITION BY 구역_기준 -- 어떤 단위로 나눌지 (채널별, 카테고리별 등) ORDER BY 정렬_기준 DESC -- 그 안에서 어떤 순서로 정렬할지 )
PARTITION BY: 데이터를 나누는 기준입니다.
PARTITION BY channel이면 채널별로 구역을 만듭니다. 생략하면 전체 테이블이 하나의 구역이 됩니다.ORDER BY: 구역 안에서의 정렬 기준입니다. 순위 함수에서는 필수입니다.
ROW_NUMBER, RANK, DENSE_RANK 동점 처리 방식
세 함수 모두 순위를 매기지만, 동점 처리 방식이 다릅니다. SQLD 2과목에서 거의 매 회차 출제되는 부분입니다.
campaign_id | channel | roas |
|---|---|---|
C001 | 검색광고 | 4.2 |
C002 | SNS | 3.8 |
C003 | 디스플레이 | 3.8 |
C004 | 검색광고 | 2.1 |
SELECT campaign_id, roas, ROW_NUMBER() OVER (ORDER BY roas DESC) AS row_num, RANK() OVER (ORDER BY roas DESC) AS rnk, DENSE_RANK() OVER (ORDER BY roas DESC) AS dense_rnk FROM
campaign_id | roas | row_num | rnk | dense_rnk |
|---|---|---|---|---|
C001 | 4.2 | 1 | 1 | 1 |
C002 | 3.8 | 2 | 2 | 2 |
C003 | 3.8 | 3 | 2 | 2 |
C004 | 2.1 | 4 | 4 | 3 |
C002와 C003이 ROAS 3.8로 동점입니다. 여기서 차이가 납니다.
ROW_NUMBER: 동점이어도 각 행에 다른 번호를 부여합니다. 정확히 N개만 뽑아야 할 때 씁니다.
RANK: 동점에 같은 순위를 주고, 다음 순위를 건너뜁니다. C002=C003=2등이므로 3등은 없고 C004=4등입니다.
DENSE_RANK: 동점에 같은 순위를 주되, 다음 순위를 건너뛰지 않습니다. C002=C003=2등, C004=3등입니다.
중요포인트: RANK는 gap이 생깁니다(3등이 없음). DENSE_RANK는 gap이 없습니다(3등이 있음). 이 차이 하나가 SQLD 선택지를 가릅니다.
실무 쿼리. 채널별 ROAS 1위 추출
각 채널에서 ROAS 1위 캠페인 하나씩 추출하는 쿼리입니다.
SELECT * FROM ( SELECT campaign_id, channel, roas, ROW_NUMBER() OVER ( PARTITION BY channel ORDER BY roas DESC ) AS rn FROM campaigns WHERE DATE_FORMAT(start_date, '%Y-%m') = '2024-10' ) ranked WHERE rn = 1
쿼리 실행 흐름: ├── FROM campaigns → 원본 테이블 전체 조회 ├── PARTITION BY channel → 채널별로 구역 생성 ├── ORDER BY roas DESC → 구역 내 ROAS 내림차순 정렬 ├── ROW_NUMBER() → rn → 각 구역의 1위에 rn=1 부여 └── WHERE rn = 1 → 채널별 1위만 최종 반환
RANK 대신 ROW_NUMBER를 선택한 이유가 있습니다. RANK를 쓰면 동점인 캠페인 두 개가 모두 rn=1이 됩니다. "채널당 1개씩"이라는 조건을 만족하지 못합니다. ROW_NUMBER는 동점이어도 하나에만 1번을 부여하기 때문에 정확히 1개씩 반환합니다.
한 가지 더 주의할 점이 있습니다. 윈도우 함수 결과(rn)는 같은 쿼리 안의 WHERE로 바로 필터링할 수 없습니다. 반드시 서브쿼리나 CTE로 감싼 뒤 바깥에서 필터링해야 합니다.
시험에 나오는 방식
SQLD 2과목에서 윈도우 함수 문제는 거의 매 회차 출제됩니다. 핵심 포인트를 정리합니다.
OVER()없이는 윈도우 함수가 아닙니다. GROUP BY와 혼동하지 않도록 주의합니다.PARTITION BY를 생략하면 전체 테이블이 하나의 구역으로 처리됩니다.
RANK는 gap 발생, DENSE_RANK는 gap 없음 — 선택지를 가르는 핵심입니다.
윈도우 함수 결과는 WHERE로 바로 필터링할 수 없습니다. 서브쿼리나 CTE가 필요합니다.
ROW_NUMBER는 동점이 있어도 반드시 고유한 번호를 부여합니다.
마치며
윈도우 함수의 핵심은 하나입니다. GROUP BY처럼 데이터를 압축하지 않고, 원본 행을 살린 채로 계산 결과를 추가한다는 것입니다. PARTITION BY로 구역을 나누고 ORDER BY로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.

채널별 ROAS 상위 캠페인을 뽑아달라는 요청 앞에서, SQL을 어느 정도 다루는 마케터도 멈추는 지점이 있습니다.
GROUP BY로 채널별 평균 ROAS는 계산할 수 있습니다. 그런데 각 채널 안에서 1위 캠페인 하나씩을 꺼내는 순간, GROUP BY는 한계에 부딪힙니다. 이 문제를 SQL 한 쿼리로 해결하는 것이 윈도우 함수(Window Function)입니다.
GROUP BY가 막히는 지점
GROUP BY는 데이터를 압축합니다. 채널별로 집계할 수 있지만, 그 과정에서 개별 캠페인 정보가 사라집니다.
-- 채널별 평균 ROAS — 가능하다 -- 그러나 '어떤 캠페인인지'는 알 수 없다 SELECT channel, AVG(roas) AS avg_roas FROM campaigns GROUP BY
결과로는 "검색광고 채널 평균 ROAS = 3.5"를 알 수 있습니다. 하지만 "검색광고 채널에서 ROAS가 가장 높은 캠페인이 무엇인지"는 알 수 없습니다. 집계되는 순간 개별 행이 사라지기 때문입니다.
이 지점에서 윈도우 함수가 필요해집니다.
윈도우 함수. 행을 유지하면서 계산을 추가한다
윈도우 함수는 GROUP BY와 달리 개별 행을 그대로 유지합니다. 원본 데이터를 살려두면서, 순위 컬럼 같은 계산 결과를 덧붙입니다.
-- 채널별 ROAS 순위를 컬럼으로 추가한다 SELECT campaign_id, channel, roas, RANK() OVER (PARTITION BY channel ORDER BY roas DESC) AS channel_rank FROM
OVER() 안의 구조가 핵심입니다.
함수명() OVER ( PARTITION BY 구역_기준 -- 어떤 단위로 나눌지 (채널별, 카테고리별 등) ORDER BY 정렬_기준 DESC -- 그 안에서 어떤 순서로 정렬할지 )
PARTITION BY: 데이터를 나누는 기준입니다.
PARTITION BY channel이면 채널별로 구역을 만듭니다. 생략하면 전체 테이블이 하나의 구역이 됩니다.ORDER BY: 구역 안에서의 정렬 기준입니다. 순위 함수에서는 필수입니다.
ROW_NUMBER, RANK, DENSE_RANK 동점 처리 방식
세 함수 모두 순위를 매기지만, 동점 처리 방식이 다릅니다. SQLD 2과목에서 거의 매 회차 출제되는 부분입니다.
campaign_id | channel | roas |
|---|---|---|
C001 | 검색광고 | 4.2 |
C002 | SNS | 3.8 |
C003 | 디스플레이 | 3.8 |
C004 | 검색광고 | 2.1 |
SELECT campaign_id, roas, ROW_NUMBER() OVER (ORDER BY roas DESC) AS row_num, RANK() OVER (ORDER BY roas DESC) AS rnk, DENSE_RANK() OVER (ORDER BY roas DESC) AS dense_rnk FROM
campaign_id | roas | row_num | rnk | dense_rnk |
|---|---|---|---|---|
C001 | 4.2 | 1 | 1 | 1 |
C002 | 3.8 | 2 | 2 | 2 |
C003 | 3.8 | 3 | 2 | 2 |
C004 | 2.1 | 4 | 4 | 3 |
C002와 C003이 ROAS 3.8로 동점입니다. 여기서 차이가 납니다.
ROW_NUMBER: 동점이어도 각 행에 다른 번호를 부여합니다. 정확히 N개만 뽑아야 할 때 씁니다.
RANK: 동점에 같은 순위를 주고, 다음 순위를 건너뜁니다. C002=C003=2등이므로 3등은 없고 C004=4등입니다.
DENSE_RANK: 동점에 같은 순위를 주되, 다음 순위를 건너뛰지 않습니다. C002=C003=2등, C004=3등입니다.
중요포인트: RANK는 gap이 생깁니다(3등이 없음). DENSE_RANK는 gap이 없습니다(3등이 있음). 이 차이 하나가 SQLD 선택지를 가릅니다.
실무 쿼리. 채널별 ROAS 1위 추출
각 채널에서 ROAS 1위 캠페인 하나씩 추출하는 쿼리입니다.
SELECT * FROM ( SELECT campaign_id, channel, roas, ROW_NUMBER() OVER ( PARTITION BY channel ORDER BY roas DESC ) AS rn FROM campaigns WHERE DATE_FORMAT(start_date, '%Y-%m') = '2024-10' ) ranked WHERE rn = 1
쿼리 실행 흐름: ├── FROM campaigns → 원본 테이블 전체 조회 ├── PARTITION BY channel → 채널별로 구역 생성 ├── ORDER BY roas DESC → 구역 내 ROAS 내림차순 정렬 ├── ROW_NUMBER() → rn → 각 구역의 1위에 rn=1 부여 └── WHERE rn = 1 → 채널별 1위만 최종 반환
RANK 대신 ROW_NUMBER를 선택한 이유가 있습니다. RANK를 쓰면 동점인 캠페인 두 개가 모두 rn=1이 됩니다. "채널당 1개씩"이라는 조건을 만족하지 못합니다. ROW_NUMBER는 동점이어도 하나에만 1번을 부여하기 때문에 정확히 1개씩 반환합니다.
한 가지 더 주의할 점이 있습니다. 윈도우 함수 결과(rn)는 같은 쿼리 안의 WHERE로 바로 필터링할 수 없습니다. 반드시 서브쿼리나 CTE로 감싼 뒤 바깥에서 필터링해야 합니다.
시험에 나오는 방식
SQLD 2과목에서 윈도우 함수 문제는 거의 매 회차 출제됩니다. 핵심 포인트를 정리합니다.
OVER()없이는 윈도우 함수가 아닙니다. GROUP BY와 혼동하지 않도록 주의합니다.PARTITION BY를 생략하면 전체 테이블이 하나의 구역으로 처리됩니다.
RANK는 gap 발생, DENSE_RANK는 gap 없음 — 선택지를 가르는 핵심입니다.
윈도우 함수 결과는 WHERE로 바로 필터링할 수 없습니다. 서브쿼리나 CTE가 필요합니다.
ROW_NUMBER는 동점이 있어도 반드시 고유한 번호를 부여합니다.
마치며
윈도우 함수의 핵심은 하나입니다. GROUP BY처럼 데이터를 압축하지 않고, 원본 행을 살린 채로 계산 결과를 추가한다는 것입니다. PARTITION BY로 구역을 나누고 ORDER BY로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.


실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
커리어
2026년 7월 11일

실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
커리어
2026년 7월 11일

[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
RANK·DENSE_RANK·ROW_NUMBER 한 번에 정리
데이터 분석
2026년 7월 10일

[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
RANK·DENSE_RANK·ROW_NUMBER 한 번에 정리
데이터 분석
2026년 7월 10일

메타 광고 초보자를 위한 타겟 설정 가이드
어드밴티지+, 상세 타겟팅, 맞춤 타겟, 유사 타겟까지 구조부터 이해해봅니다.
팀 스토리
2026년 7월 9일

메타 광고 초보자를 위한 타겟 설정 가이드
어드밴티지+, 상세 타겟팅, 맞춤 타겟, 유사 타겟까지 구조부터 이해해봅니다.
팀 스토리
2026년 7월 9일

실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
실제 공고로 본 기업이 진짜 원하는 신입 마케터는?
커리어
2026년 7월 11일

[SQLD] 윈도우 함수 하나면 채널별 성과를 직접 뽑을 수 있다
RANK·DENSE_RANK·ROW_NUMBER 한 번에 정리
데이터 분석
2026년 7월 10일

메타 광고 초보자를 위한 타겟 설정 가이드
어드밴티지+, 상세 타겟팅, 맞춤 타겟, 유사 타겟까지 구조부터 이해해봅니다.
팀 스토리
2026년 7월 9일

그로스마케터들이 퍼널 대신 '루프'를 말하기 시작한 이유
퍼널의 한계를 넘어서는 그로스 루프의 작동 원리
마케팅
2026년 7월 8일