데이터 분석

[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로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.

출처: https://unsplash.com/photos/IgUR1iX0mqM

채널별 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로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.

출처: https://unsplash.com/photos/IgUR1iX0mqM

채널별 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로 정렬 기준을 정하면, 채널별이든 카테고리별이든 담당자별이든 같은 패턴으로 응용할 수 있습니다.

출처: https://unsplash.com/photos/IgUR1iX0mqM

Read More

Read More

위그로스 Wegrowth®

대표: 이성봉 l 사업자등록번호: 518-71-00476

주소: 서울시 강남구 테헤란로128, 2층 222호

이메일: contact@wegrowth.kr

통신판매업신고: 2023-서울강남-04765

Copyright ©2026 WEGROWTH® All rights reserved.

위그로스 Wegrowth®

대표: 이성봉 l 사업자등록번호: 518-71-00476 l  주소: 서울시 강남구 테헤란로128, 2층 222호

이메일: contact@wegrowth.kr   l   통신판매업신고: 2023-서울강남-04765

Copyright ©2026 WEGROWTH® All rights reserved.