개요와 이 글에서 얻어갈 것
ABAP 개발자라면 한 번쯤 "랭킹을 구하려고 SELECT 결과를 내부 테이블에 담고 SORT한 뒤 LOOP를 돌리며 카운터를 증가시킨" 경험이 있을 것입니다. 이런 방식은 작동은 하지만 데이터가 수십만 건을 넘어가면 애플리케이션 서버 메모리와 네트워크 전송량을 잡아먹는 주범이 됩니다. SAP S/4HANA 환경에서 Open SQL(현재는 ABAP SQL로도 불림)이 윈도우 함수를 지원하면서 이 문제는 데이터베이스 레벨에서 바로 해결할 수 있게 되었습니다.
이 글에서 다룰 항목 체크리스트입니다.
- RANK(), DENSE_RANK(), ROW_NUMBER()의 결정적 차이를 매출 데이터로 비교
- SUM() OVER (PARTITION BY ... ORDER BY ...)로 누적 매출 산출
- LOOP AT GROUP BY 방식과의 성능·가독성 비교
- 다중 윈도우 함수를 한 SELECT 안에서 사용하는 패턴
- S/4HANA 1809 이후 버전 요구사항과 호환성 이슈
먼저 익혀두면 좋은 배경 지식
이 글은 advanced 난이도이므로 다음 항목에 익숙하다고 가정합니다. ABAP SQL의 기본 SELECT 문법, 인라인 선언(@DATA), CDS View의 존재와 차이, GROUP BY와 집계 함수(SUM, COUNT, AVG)의 기본 동작, 그리고 SAP HANA 또는 AnyDB 환경에서 ABAP SQL이 어떻게 SQL로 푸시다운되는지에 대한 개념입니다. 추가로 SE16, SAT(런타임 분석), ST05(SQL Trace)를 다룰 줄 알면 성능 비교 단계에서 큰 도움이 됩니다.
환경, 릴리스, 준비물
윈도우 함수는 ABAP SQL에서 비교적 늦게 도입된 기능입니다. 일반적으로 다음 환경에서 동작이 권장됩니다.
- ABAP Platform: AS ABAP 7.53 이상 (SAP_BASIS 753+)
- S/4HANA: 1809 이상 권장, 2020 이후에서 안정적 지원
- 데이터베이스: SAP HANA 권장. AnyDB(Oracle, DB2, MSSQL)에서는 일부 윈도우 함수가 제한되거나 지원되지 않을 수 있음
- 개발 도구: ADT(ABAP Development Tools in Eclipse) 권장, SE80도 가능하나 신규 문법 경고가 잦음
- 테이블 데이터: 예제에서는 가상의 ZSO_HEADER(영업 오더 헤더), ZSO_AGENT(영업 사원)를 사용한다고 가정
ABAP 7.40 이하에서는 윈도우 함수를 ABAP SQL에서 직접 쓸 수 없으며, AMDP(ABAP Managed Database Procedure)로 우회해야 합니다. 또한 ECC 환경에서는 윈도우 함수를 사용할 수 없는 경우가 많습니다.
윈도우 함수의 핵심 개념
윈도우 함수를 가장 쉽게 비유하면 "GROUP BY는 결과 행을 합치지만, 윈도우 함수는 행을 합치지 않고 각 행 옆에 같은 그룹 안에서의 계산 결과를 덧붙인다"는 것입니다. 예를 들어 영업 사원별로 매출을 합산하면 GROUP BY는 사원 1명당 1행만 남기지만, 윈도우 함수는 매 오더 행마다 "이 사원의 누적 매출은 얼마"를 옆에 붙여줍니다.
구문은 다음과 같이 구성됩니다.
집계함수(컬럼) OVER ( PARTITION BY 그룹키 ORDER BY 정렬키 )
여기서 PARTITION BY는 GROUP BY와 비슷한 역할로 "어떤 단위로 묶을지"를 정하고, ORDER BY는 윈도우 안에서 행의 순서를 정의합니다. 순위 함수의 차이를 표로 정리합니다.
| 함수 | 동점 처리 | 다음 순위 점프 | 활용 예시 |
|---|---|---|---|
| ROW_NUMBER() | 임의로 1, 2 부여 | 없음 (연속) | 페이징, 중복 제거 |
| RANK() | 동일 순위 부여 | 건수만큼 점프(1,1,3) | 스포츠 랭킹, 경쟁 순위 |
| DENSE_RANK() | 동일 순위 부여 | 점프 없음(1,1,2) | 등급 분류, 가격대 구간 |
여기에 누적합을 구하는 SUM() OVER가 결합되면, 데이터베이스 한 번의 SELECT만으로 정렬+순위+누적값을 한꺼번에 얻을 수 있어 ABAP 애플리케이션 서버의 부담을 크게 줄입니다.
실전 예제 1단계: 기본 순위 매기기
가장 단순한 형태부터 시작합니다. 영업 사원(agent_id)별 총 매출(net_amount 합계)을 구하고, 매출이 큰 순서로 순위를 부여합니다. 내부 테이블 LOOP나 SORT 없이 SELECT 한 번으로 끝낸다는 점이 핵심입니다.
SELECT agent_id,
SUM( net_amount ) AS total_sales,
RANK( ) OVER ( ORDER BY SUM( net_amount ) DESCENDING ) AS sales_rank
FROM zso_header
WHERE fiscal_year = '2026'
GROUP BY agent_id
INTO TABLE @DATA(lt_rank).
LOOP AT lt_rank INTO DATA(ls_rank).
WRITE: / ls_rank-sales_rank, ls_rank-agent_id, ls_rank-total_sales.
ENDLOOP.
주목할 점은 ORDER BY 절 안에 SUM( net_amount )을 그대로 쓸 수 있다는 것입니다. GROUP BY가 먼저 처리된 후 윈도우 함수가 그 위에서 동작하므로, 별도의 서브쿼리 없이도 집계값에 대한 순위 부여가 가능합니다. DESCENDING은 ABAP SQL식 표기이며, 일부 환경에서는 DESC도 허용됩니다.
실전 예제 2단계: 지역별 파티션 + 누적합 + 다중 윈도우
실무에서는 보통 "지역별로 사원 순위를 매기고, 각 지역 내 누적 매출도 함께 보고 싶다"는 요구가 들어옵니다. PARTITION BY와 SUM() OVER를 결합하고, 에러 처리·로깅까지 포함한 형태로 작성합니다.
TRY.
SELECT h~region,
h~agent_id,
SUM( h~net_amount ) AS agent_sales,
RANK( ) OVER ( PARTITION BY h~region
ORDER BY SUM( h~net_amount ) DESCENDING ) AS region_rank,
DENSE_RANK( ) OVER ( PARTITION BY h~region
ORDER BY SUM( h~net_amount ) DESCENDING ) AS region_drank,
SUM( SUM( h~net_amount ) ) OVER ( PARTITION BY h~region
ORDER BY SUM( h~net_amount ) DESCENDING
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW ) AS region_cum_sales
FROM zso_header AS h
WHERE h~fiscal_year = @iv_year
GROUP BY h~region, h~agent_id
ORDER BY h~region, region_rank
INTO TABLE @DATA(lt_report).
IF lt_report IS INITIAL.
MESSAGE |No sales data for { iv_year }| TYPE 'I'.
RETURN.
ENDIF.
CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
cl_demo_output=>display( lx_sql->get_text( ) ).
ENDTRY.
여기서 가장 어색해 보이는 부분이 SUM( SUM( h~net_amount ) ) OVER (...)입니다. 안쪽 SUM은 GROUP BY로 묶인 사원별 매출 집계이고, 바깥 SUM은 그 집계값을 다시 윈도우 안에서 누적하는 역할입니다. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW는 "윈도우의 시작부터 현재 행까지" 합산하라는 의미로, 누적합을 명시적으로 정의할 때 사용합니다. 생략하면 DB 종류에 따라 기본 프레임이 달라질 수 있어 명시를 권장합니다.
실전 예제 3단계: 프로덕션 패턴과 LOOP 방식 비교
프로덕션 코드에서는 보통 CDS View로 캡슐화해 재사용성과 권한 검사(DCL)를 함께 챙깁니다. 윈도우 함수 결과를 CDS Projection View로 노출하고, 그 위에서 OData나 ABAP RAP가 소비하는 패턴입니다.
@AbapCatalog.sqlViewName: 'ZVSALESRANK'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Ranking with Window Functions'
define view Z_C_SalesRank
with parameters
p_year : abap.numc(4)
as select from zso_header as h
{
key h.region,
key h.agent_id,
sum( h.net_amount ) as AgentSales,
rank() over ( partition by h.region
order by sum( h.net_amount ) desc ) as RegionRank,
sum( sum( h.net_amount ) ) over ( partition by h.region
order by sum( h.net_amount ) desc
rows between unbounded preceding
and current row ) as RegionCumSales
}
where h.fiscal_year = :p_year
group by h.region, h.agent_id
LOOP 기반 방식과의 성능 비교 패턴은 다음과 같습니다.
" 방식 A: 윈도우 함수 (DB 푸시다운)
GET RUN TIME FIELD DATA(lv_t1).
SELECT * FROM z_c_salesrank( p_year = '2026' )
INTO TABLE @DATA(lt_a).
GET RUN TIME FIELD DATA(lv_t2).
" 방식 B: 전통적 내부 테이블 + LOOP
GET RUN TIME FIELD DATA(lv_t3).
SELECT region, agent_id, SUM( net_amount ) AS agent_sales
FROM zso_header
WHERE fiscal_year = '2026'
GROUP BY region, agent_id
ORDER BY region, agent_sales DESCENDING
INTO TABLE @DATA(lt_raw).
DATA: lv_rank TYPE i, lv_cum TYPE p LENGTH 15 DECIMALS 2,
lv_prev_region TYPE zso_header-region.
LOOP AT lt_raw INTO DATA(ls_raw).
IF ls_raw-region <> lv_prev_region.
lv_rank = 0. lv_cum = 0. lv_prev_region = ls_raw-region.
ENDIF.
lv_rank = lv_rank + 1.
lv_cum = lv_cum + ls_raw-agent_sales.
ENDLOOP.
GET RUN TIME FIELD DATA(lv_t4).
WRITE: / 'Window function (us):', lv_t2 - lv_t1,
/ 'Loop based (us):', lv_t4 - lv_t3.
HANA 환경에서 수십만~수백만 건 규모의 데이터에서는 윈도우 함수 방식이 LOOP 방식 대비 큰 폭으로 빠른 경우가 일반적입니다. 네트워크로 raw 데이터를 끌어오지 않고 DB가 정렬·순위·누적까지 처리하기 때문입니다.
흔한 실수와 트러블슈팅 FAQ
현장에서 반복적으로 마주치는 함정과 해결책입니다.
Q1. ORDER BY 없이 RANK()를 썼는데 syntax error가 납니다.
RANK(), DENSE_RANK(), ROW_NUMBER()는 ORDER BY가 필수입니다. 반대로 SUM() OVER에서는 ORDER BY가 없으면 "파티션 전체 합"이 되고, ORDER BY가 있으면 "누적합"이 됩니다. 이 차이를 모르면 의도와 다른 결과가 나옵니다.
Q2. GROUP BY와 충돌해서 컴파일 오류가 납니다.
GROUP BY 절에 포함되지 않은 컬럼은 SELECT 리스트에 그대로 쓸 수 없습니다. 윈도우 함수 안에서 SUM( column )을 다시 감싸거나, GROUP BY 키 자체를 PARTITION BY에 사용해야 합니다.
Q3. AnyDB에서 런타임 dump가 발생합니다.
ABAP SQL의 윈도우 함수는 데이터베이스가 해당 기능을 지원해야 합니다. HANA에서는 잘 동작하지만 일부 AnyDB에서는 푸시다운 자체가 불가능합니다. SY-DBSYS를 체크해 fallback 로직(LOOP 기반)을 분기 처리하거나, 시스템 운영팀과 DB 버전을 먼저 확인하는 것이 안전합니다.
ROWS BETWEEN 프레임을 명시하지 않아 DB 종류에 따라 기본값이 달라져 누적합 결과가 틀려지는 경우, ORDER BY 정렬키에 NULL 가능 컬럼을 사용해 순위가 흔들리는 경우도 자주 봅니다. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 명시와 COALESCE 보정을 습관화하면 이런 문제를 피할 수 있습니다.
이후 확장해볼 만한 주제
LAG()/LEAD()를 활용한 기간 비교(전월 매출 대비 증감), NTILE()을 이용한 4분위 등급 분류, FIRST_VALUE()/LAST_VALUE()로 그룹 내 대표값 추출 등을 다뤄볼 수 있습니다. CDS View Entity(2020 이후 신규 문법)와 결합하면 RAP 기반 Fiori 앱에서 실시간 랭킹 대시보드를 구축할 수 있습니다. 더 나아가 AMDP로 복잡한 윈도우 + 셀프 조인 패턴을 처리하거나, Calculation View 레벨에서 해결하는 것이 더 적합한 경우도 있습니다. 상황에 따라 도구를 선택하는 감각을 익히는 것이 중요합니다.
댓글 0
아직 댓글이 없습니다.