SAP HANA Cloud SQL Window Function — RANK·LEAD·LAG·NTILE로 판매 분석 자동화하기

Moderator · 조회 2

SAP HANA Cloud SQL Window Function -- RANK, LEAD, LAG, NTILE로 판매 분석 자동화하기

1. 개요 및 학습 목표

SAP HANA Cloud SQL에서 제공하는 Window Function(윈도우 함수)은 행 단위 분석을 집계 없이 수행할 수 있는 SQL의 핵심 기능입니다. 이 튜토리얼에서는 판매 데이터를 대상으로 RANK, DENSE_RANK, ROW_NUMBER, LEAD, LAG, NTILE 함수를 활용하여 지역별 순위 산정, 전월 대비(MoM) 성장률 계산, 성과급 구간 분류까지 실무 시나리오를 단계별로 다룹니다.

2. 선수 지식

이 튜토리얼을 진행하려면 다음 지식이 필요합니다.

3. 환경 / 버전 / 준비물

본 튜토리얼에서 사용하는 환경은 다음과 같습니다.

항목사양
데이터베이스SAP HANA Cloud (QRC 1/2024 이상 권장)
BTP 에디션Free Tier 또는 Enterprise (HANA Cloud 인스턴스 포함)
SQL 실행 환경SAP HANA Database Explorer (HANA Cockpit) 또는 SAP Business Application Studio
접근 경로BTP Cockpit > HANA Cloud Instances > Open SQL Console

사전 준비: SAP BTP 계정이 없다면 SAP HANA Cloud Free Tier 시작 가이드를 참고하여 인스턴스를 생성합니다. QRC 1/2024 이상 버전에서는 Calculation View의 Window 노드에서도 LAG, LEAD, DENSE_RANK 등을 사용할 수 있으므로 가능하면 최신 버전을 사용하는 것이 좋습니다.

4. 핵심 개념

Window Function이란?

Window Function은 OVER() 절을 통해 현재 행과 관련된 행 집합(윈도우)에 대해 분석 연산을 수행하는 함수입니다. 일반적인 GROUP BY 집계와 가장 큰 차이는 결과 행 수를 줄이지 않는다는 점입니다.

비유하자면, GROUP BY는 학급 전체 학생을 반별로 묶어 반 평균만 남기는 것이고, Window Function은 각 학생의 성적표에 "반 평균"과 "반 내 석차"를 추가 열로 기록해주는 것입니다. 원본 행은 그대로 유지됩니다.

OVER() 절 구조

함수명() OVER (
  PARTITION BY 구분_컬럼    -- 윈도우를 나누는 기준 (생략 시 전체가 하나의 파티션)
  ORDER BY 정렬_컬럼       -- 파티션 내 정렬 기준
  ROWS/RANGE BETWEEN ...  -- 프레임 범위 (선택)
)

Window Function vs GROUP BY 비교

구분GROUP BYWindow Function
결과 행 수그룹 수만큼 축소원본 행 수 유지
개별 행 데이터접근 불가 (집계만 가능)개별 행 + 분석 결과 동시 표시
이전/다음 행 참조불가LAG/LEAD로 가능
순위 계산별도 서브쿼리 필요RANK/DENSE_RANK로 직접 가능
적용 시점FROM, WHERE 이후GROUP BY 이후, ORDER BY 이전

RANK vs DENSE_RANK vs ROW_NUMBER 비교

판매액이 동일한 경우(동점)의 처리 방식이 세 함수의 핵심 차이입니다.

판매원판매액ROW_NUMBERRANKDENSE_RANK
김철수500111
이영희500211
박민수300332
최지은200443

5. 실전 코드 3단계

1단계: 기본 예제 -- 샘플 데이터와 순위 함수

먼저 실습용 판매 데이터 테이블을 생성하고, 세 가지 순위 함수를 비교합니다.

-- 샘플 테이블 생성
CREATE TABLE SALES_2026 (
    SALE_ID       INTEGER PRIMARY KEY,
    SALESPERSON   NVARCHAR(50),
    REGION        NVARCHAR(20),
    PRODUCT_ID    NVARCHAR(10),
    SALE_MONTH    INTEGER,
    SALES_AMOUNT  DECIMAL(15,2)
);

-- 테스트 데이터 삽입
INSERT INTO SALES_2026 VALUES (1, '김철수', '서울', 'P001', 1, 12000);
INSERT INTO SALES_2026 VALUES (2, '이영희', '서울', 'P002', 1, 15000);
INSERT INTO SALES_2026 VALUES (3, '박민수', '부산', 'P001', 1, 15000);
INSERT INTO SALES_2026 VALUES (4, '최지은', '부산', 'P003', 1, 9000);
INSERT INTO SALES_2026 VALUES (5, '김철수', '서울', 'P001', 2, 14000);
INSERT INTO SALES_2026 VALUES (6, '이영희', '서울', 'P002', 2, 13000);
INSERT INTO SALES_2026 VALUES (7, '박민수', '부산', 'P001', 2, 17000);
INSERT INTO SALES_2026 VALUES (8, '최지은', '부산', 'P003', 2, 11000);
INSERT INTO SALES_2026 VALUES (9, '김철수', '서울', 'P001', 3, 16000);
INSERT INTO SALES_2026 VALUES (10, '이영희', '서울', 'P002', 3, 16000);
INSERT INTO SALES_2026 VALUES (11, '박민수', '부산', 'P001', 3, 20000);
INSERT INTO SALES_2026 VALUES (12, '최지은', '부산', 'P003', 3, 12000);
-- 세 가지 순위 함수 비교 (전체 판매원 대상)
SELECT
    SALESPERSON,
    SUM(SALES_AMOUNT) AS TOTAL_SALES,
    ROW_NUMBER() OVER (ORDER BY SUM(SALES_AMOUNT) DESC) AS ROW_NUM,
    RANK()       OVER (ORDER BY SUM(SALES_AMOUNT) DESC) AS RANK_NUM,
    DENSE_RANK() OVER (ORDER BY SUM(SALES_AMOUNT) DESC) AS DENSE_RANK_NUM
FROM SALES_2026
GROUP BY SALESPERSON;

위 쿼리에서 주의할 점은 Window Function이 GROUP BY 이후에 적용된다는 것입니다. 따라서 SUM(SALES_AMOUNT)라는 집계 결과를 기준으로 순위를 매길 수 있습니다.

2단계: 실무 시나리오 -- 지역별 TOP 3, MoM 성장률

시나리오 A: 지역별 월간 판매 TOP 3 추출

서브쿼리(인라인 뷰)를 사용하여 RANK 결과를 필터링합니다. Window Function은 WHERE 절에서 직접 사용할 수 없으므로 반드시 서브쿼리로 감싸야 합니다.

-- 지역별 판매 TOP 3 (전체 기간 합산 기준)
SELECT REGION, SALESPERSON, TOTAL_SALES, REGIONAL_RANK
FROM (
    SELECT
        REGION,
        SALESPERSON,
        SUM(SALES_AMOUNT) AS TOTAL_SALES,
        RANK() OVER (
            PARTITION BY REGION
            ORDER BY SUM(SALES_AMOUNT) DESC
        ) AS REGIONAL_RANK
    FROM SALES_2026
    GROUP BY REGION, SALESPERSON
)
WHERE REGIONAL_RANK <= 3
ORDER BY REGION, REGIONAL_RANK;

시나리오 B: 판매원별 MoM(전월 대비) 성장률 계산

LAG 함수를 활용하여 이전 월 판매액을 참조하고, 성장률을 퍼센트로 계산합니다. 첫 번째 월에는 이전 데이터가 없으므로 NULL 처리에 주의합니다.

-- 판매원별 MoM 성장률
SELECT
    SALESPERSON,
    SALE_MONTH,
    SALES_AMOUNT,
    LAG(SALES_AMOUNT, 1) OVER (
        PARTITION BY SALESPERSON
        ORDER BY SALE_MONTH
    ) AS PREV_MONTH_SALES,
    CASE
        WHEN LAG(SALES_AMOUNT, 1) OVER (
            PARTITION BY SALESPERSON ORDER BY SALE_MONTH
        ) IS NULL THEN NULL
        WHEN LAG(SALES_AMOUNT, 1) OVER (
            PARTITION BY SALESPERSON ORDER BY SALE_MONTH
        ) = 0 THEN NULL
        ELSE ROUND(
            (SALES_AMOUNT - LAG(SALES_AMOUNT, 1) OVER (
                PARTITION BY SALESPERSON ORDER BY SALE_MONTH
            )) * 100.0
            / LAG(SALES_AMOUNT, 1) OVER (
                PARTITION BY SALESPERSON ORDER BY SALE_MONTH
            ), 2)
    END AS MOM_GROWTH_PCT
FROM SALES_2026
ORDER BY SALESPERSON, SALE_MONTH;

NULLIF 활용 팁: 위 CASE 문 대신 NULLIF(LAG(...), 0)을 분모에 사용하면 0으로 나누기 오류를 간결하게 방지할 수 있습니다. 다만 첫 행의 NULL 반환은 별도 처리가 필요합니다.

3단계: 프로덕션 -- 성과급 분류, 누적 매출, 뷰 생성

시나리오 C: NTILE로 성과급 등급 자동 분류

NTILE(4)를 사용하면 전체 판매원을 4분위로 나누어 성과급 등급을 자동 배정할 수 있습니다.

-- 성과급 등급 자동 분류
SELECT
    SALESPERSON,
    SUM(SALES_AMOUNT) AS TOTAL_SALES,
    NTILE(4) OVER (ORDER BY SUM(SALES_AMOUNT) DESC) AS TIER_NUM,
    CASE NTILE(4) OVER (ORDER BY SUM(SALES_AMOUNT) DESC)
        WHEN 1 THEN '플래티넘 (상위 25%) - 성과급 150%'
        WHEN 2 THEN '골드 (25~50%) - 성과급 120%'
        WHEN 3 THEN '실버 (50~75%) - 성과급 100%'
        WHEN 4 THEN '브론즈 (하위 25%) - 성과급 80%'
    END AS BONUS_TIER
FROM SALES_2026
GROUP BY SALESPERSON;

시나리오 D: 누적 매출과 종합 분석 뷰

프로덕션 환경에서는 반복적으로 사용하는 분석 쿼리를 SQL View로 만들어 재사용성과 유지보수성을 높입니다.

-- 종합 판매 분석 뷰 생성
CREATE OR REPLACE VIEW V_SALES_ANALYSIS AS
SELECT
    s.SALESPERSON,
    s.REGION,
    s.SALE_MONTH,
    s.SALES_AMOUNT,
    -- 지역 내 월간 순위
    RANK() OVER (
        PARTITION BY s.REGION, s.SALE_MONTH
        ORDER BY s.SALES_AMOUNT DESC
    ) AS REGION_MONTH_RANK,
    -- 전월 대비 차이
    s.SALES_AMOUNT - LAG(s.SALES_AMOUNT, 1, 0) OVER (
        PARTITION BY s.SALESPERSON
        ORDER BY s.SALE_MONTH
    ) AS MOM_DIFF,
    -- 누적 매출 (판매원별)
    SUM(s.SALES_AMOUNT) OVER (
        PARTITION BY s.SALESPERSON
        ORDER BY s.SALE_MONTH
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CUMULATIVE_SALES,
    -- 다음 월 매출 (예측 비교용)
    LEAD(s.SALES_AMOUNT, 1) OVER (
        PARTITION BY s.SALESPERSON
        ORDER BY s.SALE_MONTH
    ) AS NEXT_MONTH_SALES
FROM SALES_2026 s;
-- 뷰 활용: 서울 지역 1위 판매원만 조회
SELECT *
FROM V_SALES_ANALYSIS
WHERE REGION = '서울'
  AND REGION_MONTH_RANK = 1
ORDER BY SALE_MONTH;

성능 참고: Window Function은 HANA의 컬럼 스토어 엔진에서 일반적으로 효율적으로 처리됩니다. 다만 대용량 테이블에서 PARTITION BY 컬럼에 인덱스가 없으면 풀 스캔이 발생할 수 있으므로, 자주 사용하는 파티션 키에 대해 적절한 인덱스 구성을 권장합니다.

6. 흔한 실수 / 트러블슈팅

FAQ 1: "Missing ORDER BY in window function" 오류가 발생합니다

RANK, DENSE_RANK, ROW_NUMBER, NTILE 등 순위 함수는 반드시 OVER() 절 안에 ORDER BY가 있어야 합니다. PARTITION BY만 지정하고 ORDER BY를 생략하면 오류가 발생합니다.

-- 잘못된 예
RANK() OVER (PARTITION BY REGION)  -- ORDER BY 누락 -> 오류

-- 올바른 예
RANK() OVER (PARTITION BY REGION ORDER BY SALES_AMOUNT DESC)

FAQ 2: LAG 결과가 첫 행에서 NULL이 나와 계산이 깨집니다

LAG 함수는 이전 행이 없을 때 기본적으로 NULL을 반환합니다. 세 번째 인자로 기본값을 지정하거나, COALESCE 또는 CASE 문으로 NULL을 처리하세요.

-- 기본값 0 지정
LAG(SALES_AMOUNT, 1, 0) OVER (ORDER BY SALE_MONTH)

-- COALESCE 활용
COALESCE(LAG(SALES_AMOUNT, 1) OVER (ORDER BY SALE_MONTH), 0)

FAQ 3: NTILE(0)을 넣었더니 런타임 오류가 발생합니다

NTILE의 인자는 반드시 양의 정수(1 이상)여야 합니다. 0이나 음수를 넣으면 런타임 오류가 발생합니다. 동적으로 값을 전달하는 경우 입력값 검증을 추가하세요.

FAQ 4: WHERE 절에서 Window Function을 직접 사용하면 오류가 납니다

Window Function은 SQL 실행 순서상 WHERE 이후에 평가됩니다. 따라서 WHERE RANK() OVER (...) <= 3처럼 직접 사용할 수 없습니다. 반드시 서브쿼리로 감싼 뒤 외부 쿼리에서 필터링해야 합니다.

FAQ 5: GROUP BY와 Window Function을 함께 쓸 때 혼동됩니다

Window Function은 GROUP BY 이후에 적용됩니다. 따라서 RANK() OVER (ORDER BY SUM(col) DESC)처럼 집계 결과를 Window Function의 정렬 기준으로 사용하는 것이 가능합니다. 단, SELECT 절에 Window Function과 비집계 컬럼을 동시에 쓸 때는 GROUP BY에 해당 컬럼이 포함되어 있는지 확인하세요.

7. 다음 단계 / 관련 주제

Window Function을 익혔다면 다음 주제로 확장해보세요.

8. 참고 자료