1. 개요 및 핵심 포인트
대용량 테이블을 한 번에 로드하면 메모리 폭증과 응답 지연이 발생합니다. ABAP SQL에서는 OFFSET ... FETCH NEXT ... ROWS ONLY 구문으로 페이지 단위 조회가 가능하며, 페이지 깊이가 커질수록 OFFSET이 비싸지는 한계를 보완하기 위해 커서 기반 페이징(LCP, Last-Key Cursor Pagination)이 권장됩니다.
- OFFSET·FETCH의 문법과
ORDER BY강제 조건 이해 - 0-based 페이지 인덱스 → OFFSET 변환 공식 습득
- 대용량 환경에서 OFFSET 스캔 비용을 회피하는 커서 페이징 패턴 학습
- ABAP
LOOP를 이용한 페이지 누적 처리 - 인덱스 활용 여부에 따른 성능 차이 인지
2. 사전에 알아두면 좋은 배경
본 문서를 따라가려면 ABAP Open SQL/ABAP SQL의 SELECT ... INTO TABLE 기본 문법, WHERE와 ORDER BY 절, 호스트 변수(@lv_var) 표기법, 그리고 내부 테이블(STANDARD TABLE OF) 개념을 알고 있어야 합니다. 또한 ABAP CDS view와 일반 DB 테이블에서 페이징이 모두 가능하다는 점, ADBC가 아닌 ABAP SQL 레벨의 페이징을 다룬다는 점도 미리 인지하면 좋습니다.
3. 실습 환경 및 버전
- ABAP 릴리스: ABAP 7.50 SP02 이상 (OFFSET 절은 7.50부터 ABAP SQL에서 사용 가능)
- 권장 백엔드: SAP S/4HANA 2020 이상 또는 SAP BTP ABAP Environment(Steampunk) 최신
- DB: SAP HANA 2.0 권장 (AnyDB에서도 동작하나 옵티마이저 동작 차이 존재)
- IDE: ADT(ABAP Development Tools) for Eclipse
- 예시 테이블:
ZSALES_ORDER(가정 — 컬럼order_id,order_date,customer_id,amount)
OFFSET 절은 ABAP SQL의 메인 쿼리에서 ORDER BY가 명시되어야 사용할 수 있습니다. 서브쿼리/UNION 내부에서는 사용 제약이 있을 수 있으므로 릴리스 노트를 함께 확인하는 것을 권장합니다.
4. 핵심 개념과 동작 원리
4.1 OFFSET·FETCH의 동작 모델
OFFSET은 옵티마이저가 결과 집합을 정렬한 뒤 앞에서 n행을 건너뛰는 연산입니다. 즉 100,000번째 페이지를 요청하면 DB는 앞쪽 99,999 페이지에 해당하는 행을 모두 읽어 정렬한 뒤 버리는 형태로 동작합니다. 이는 페이지가 깊어질수록 비용이 선형적으로 증가하는 원인이 됩니다.
4.2 인덱스 vs 풀스캔
ORDER BY 컬럼에 적절한 인덱스(B-Tree 혹은 HANA의 정렬된 컬럼 스토어 통계)가 있으면 OFFSET 동안 인덱스 순회만으로 행을 스킵할 수 있어 비교적 가볍습니다. 인덱스가 없으면 풀스캔 후 sort 연산이 동반되며, 페이지 깊이와 무관하게 매 호출마다 큰 비용이 발생합니다.
4.3 커서 페이징(LCP) 비유
책갈피를 떠올리면 쉽습니다. OFFSET은 “1쪽부터 읽다가 N쪽에서 멈춤”인 반면, 커서 페이징은 “지난번 책갈피(마지막 키)부터 m쪽만 더 읽기”입니다. WHERE order_id > @lv_last_key 조건에 정렬 키 인덱스를 그대로 활용하므로, 페이지가 아무리 깊어도 비용이 거의 일정합니다.
[ OFFSET 방식 ]
정렬된 전체 결과 → [건너뜀 1..N] → [반환 N+1..N+m]
[ LCP 방식 ]
WHERE key > last_key → 인덱스 점프 → [반환 m건] → 새 last_key
5. 실전 코드 — 3단계
5.1 1단계: 기본 OFFSET·FETCH
가장 단순한 형태로, 첫 페이지(0번 페이지)에서 20건을 가져오는 예제입니다. ORDER BY가 반드시 선행되어야 결과의 안정성이 보장됩니다.
DATA: lt_orders TYPE STANDARD TABLE OF zsales_order,
lv_pagesize TYPE i VALUE 20,
lv_offset TYPE i VALUE 0.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
WHERE order_date GE @( cl_abap_context_info=>get_system_date( ) - 30 )
ORDER BY order_id
INTO TABLE @lt_orders
OFFSET @lv_offset
UP TO @lv_pagesize ROWS.
cl_demo_output=>display( lt_orders ).
참고로 ABAP SQL에서는 UP TO n ROWS와 OFFSET을 조합해 페이징을 구성합니다. 표준 SQL의 FETCH NEXT n ROWS ONLY에 대응되는 절은 ABAP에서 UP TO n ROWS로 표기됩니다.
5.2 2단계: 페이지 번호 계산 + 에러/로깅
실무에서는 클라이언트가 페이지 번호를 0-based 또는 1-based로 보냅니다. 변환 공식은 lv_offset = lv_page * lv_pagesize입니다. 비정상 입력 방어와 로깅을 함께 처리합니다.
METHOD get_orders_by_page.
DATA: lt_orders TYPE STANDARD TABLE OF zsales_order,
lv_offset TYPE i,
lv_pagesize TYPE i.
" 1) 입력 검증
lv_pagesize = COND #( WHEN iv_pagesize BETWEEN 1 AND 200
THEN iv_pagesize
ELSE 50 ).
IF iv_page < 0.
RAISE EXCEPTION TYPE zcx_paging_invalid
EXPORTING textid = zcx_paging_invalid=>negative_page.
ENDIF.
" 2) OFFSET 계산 (0-based)
lv_offset = iv_page * lv_pagesize.
" 3) 페이지 조회
TRY.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
WHERE customer_id = @iv_customer_id
ORDER BY order_date DESCENDING, order_id DESCENDING
INTO TABLE @lt_orders
OFFSET @lv_offset
UP TO @lv_pagesize ROWS.
" 4) 로깅
MESSAGE |Page { iv_page } size { lv_pagesize } rows { lines( lt_orders ) }|
TYPE 'I'.
CATCH cx_sy_open_sql_db INTO DATA(lx_db).
" DB 오류 처리
RAISE EXCEPTION TYPE zcx_paging_db
EXPORTING previous = lx_db.
ENDTRY.
rt_orders = lt_orders.
ENDMETHOD.
주의할 점은 정렬 키가 유일성(uniqueness)을 보장해야 한다는 것입니다. order_date만 사용하면 동일 일자에 여러 행이 존재할 때 페이지 경계에서 행이 누락되거나 중복될 수 있습니다. 그래서 위 예제에서는 order_id를 보조 키로 추가했습니다.
5.3 3단계: 커서 기반 페이징 (LCP)
페이지가 수천 단위로 깊어지면 OFFSET은 매우 느려집니다. 대용량 환경에서는 마지막 키를 기억하고 그 다음부터 조회하는 커서 패턴이 권장됩니다.
METHOD get_orders_cursor.
" iv_last_key 가 INITIAL 이면 첫 페이지 요청
DATA: lt_orders TYPE STANDARD TABLE OF zsales_order,
lv_pagesize TYPE i.
lv_pagesize = COND #( WHEN iv_pagesize BETWEEN 1 AND 200
THEN iv_pagesize ELSE 50 ).
IF iv_last_key IS INITIAL.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
WHERE customer_id = @iv_customer_id
ORDER BY order_id
INTO TABLE @lt_orders
UP TO @lv_pagesize ROWS.
ELSE.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
WHERE customer_id = @iv_customer_id
AND order_id > @iv_last_key
ORDER BY order_id
INTO TABLE @lt_orders
UP TO @lv_pagesize ROWS.
ENDIF.
" 다음 호출에 넘겨줄 last_key
IF lt_orders IS NOT INITIAL.
ev_next_key = lt_orders[ lines( lt_orders ) ]-order_id.
ELSE.
CLEAR ev_next_key. " 페이지 끝
ENDIF.
rt_orders = lt_orders.
ENDMETHOD.
핵심은 WHERE order_id > @iv_last_key + ORDER BY order_id + UP TO n ROWS 조합입니다. order_id가 PK 또는 유니크 인덱스이면 매 호출이 인덱스 점프 한 번으로 끝나므로 페이지 깊이와 비용이 무관해집니다. 다중 컬럼 정렬이 필요하면 (sort_col, pk) > (@last_sort, @last_pk) 형태의 튜플 비교를 직접 분해해 구현합니다.
6. 실전 패턴 — ABAP LOOP로 전체 페이징 처리
대량 마이그레이션, 야간 배치, IDoc 송신 등에서는 페이지를 순회하며 누적 처리해야 합니다. OFFSET 방식과 LCP 방식 두 가지의 누적 패턴을 비교합니다.
6.1 OFFSET 누적 (소~중 규모)
DATA: lt_page TYPE STANDARD TABLE OF zsales_order,
lt_total TYPE STANDARD TABLE OF zsales_order,
lv_page TYPE i VALUE 0,
lv_size TYPE i VALUE 500.
DO.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
ORDER BY order_id
INTO TABLE @lt_page
OFFSET @( lv_page * lv_size )
UP TO @lv_size ROWS.
IF lt_page IS INITIAL.
EXIT.
ENDIF.
APPEND LINES OF lt_page TO lt_total.
lv_page = lv_page + 1.
ENDDO.
6.2 LCP 누적 (대규모 권장)
DATA: lt_page TYPE STANDARD TABLE OF zsales_order,
lv_lastkey TYPE zsales_order-order_id,
lv_size TYPE i VALUE 1000.
DO.
IF lv_lastkey IS INITIAL.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
ORDER BY order_id
INTO TABLE @lt_page
UP TO @lv_size ROWS.
ELSE.
SELECT order_id, order_date, customer_id, amount
FROM zsales_order
WHERE order_id > @lv_lastkey
ORDER BY order_id
INTO TABLE @lt_page
UP TO @lv_size ROWS.
ENDIF.
IF lt_page IS INITIAL.
EXIT.
ENDIF.
" 처리 로직 — 청크 단위 commit/log
PERFORM process_chunk USING lt_page.
lv_lastkey = lt_page[ lines( lt_page ) ]-order_id.
ENDDO.
OFFSET 누적은 코드가 직관적이지만 페이지 깊이가 깊어질수록 누적 비용이 큽니다. LCP 누적은 매 청크 비용이 일정하므로 백만 건 이상의 배치에 일반적으로 더 적합합니다.
7. 흔한 실수와 트러블슈팅
FAQ 1. ORDER BY를 안 썼더니 페이지 결과가 매번 달라요
OFFSET·FETCH는 정렬된 결과 집합 위에서 동작합니다. ORDER BY가 없으면 DB는 결과 순서를 보장하지 않으므로 같은 OFFSET 값에도 다른 행이 반환될 수 있습니다. ABAP SQL은 OFFSET 사용 시 ORDER BY를 사실상 필수로 요구하며, 정렬 키는 유일성까지 갖춰야 페이지 경계에서 누락/중복이 없습니다.
FAQ 2. 페이지 번호가 커질수록 응답이 점점 느려져요
OFFSET의 본질적 한계입니다. 100페이지 → 1,000페이지 → 10,000페이지로 깊어질수록 DB가 스킵해야 할 행 수가 선형으로 증가합니다. 일반적으로 다음 순서로 검토하는 것을 권장합니다.
- ORDER BY 컬럼에 인덱스가 있는지 확인 (HANA: 정렬 키 활용)
- WHERE 절로 결과 집합 자체를 줄였는지 확인
- 그래도 깊은 페이지가 필요하면 커서 기반 페이징(LCP)으로 전환
FAQ 3. 커서 페이징에서 last_key가 NULL/INITIAL일 때 처리는?
첫 호출에는 마지막 키가 없으므로 WHERE key > @last_key 조건 자체를 빼야 합니다. IS INITIAL로 분기 처리하지 않고 무조건 조건을 붙이면 ABAP 호스트 변수의 기본값(예: 빈 문자열, 0) 때문에 결과가 왜곡될 수 있습니다. 또한 페이지 끝 도달 시 ev_next_key를 명시적으로 CLEAR하여 클라이언트가 “더 이상 페이지 없음”을 인지하도록 신호를 주는 패턴이 유용합니다.
FAQ 4. 다중 컬럼 정렬에서 LCP가 어렵습니다
예를 들어 ORDER BY order_date DESC, order_id DESC 라면 단일 컬럼 비교로는 표현이 안 됩니다. 튜플 비교를 분해해서 WHERE order_date < @last_date OR ( order_date = @last_date AND order_id < @last_id ) 형태로 작성하면 됩니다. 이 패턴은 인덱스를 그대로 활용할 수 있도록 정렬 순서와 동일하게 맞추는 것이 중요합니다.
FAQ 5. CDS view에서도 동일하게 동작하나요?
ABAP CDS view에 대한 SELECT에서도 ORDER BY ... OFFSET ... UP TO n ROWS 구문은 일반적으로 사용 가능합니다. 다만 CDS 어노테이션, 클라이언트 처리(@ClientHandling), 권한 체크(WITH PRIVILEGED ACCESS) 등이 옵티마이저 계획에 영향을 줄 수 있으므로, 성능 테스트는 실제 사용 컨텍스트에서 진행하는 것을 권장합니다.
8. 참고 자료
- ABAP Keyword Documentation — SELECT, OFFSET (help.sap.com)
- ABAP Keyword Documentation — SELECT, UP TO n ROWS (help.sap.com)
- ABAP Keyword Documentation — ORDER BY 절 (help.sap.com)
- ABAP Platform — Open SQL Performance Guidelines (help.sap.com)
- SAP HANA SQL Reference — LIMIT/OFFSET 동작 (help.sap.com)
- SAP Community Blogs — ABAP SQL 태그
- SAP Learning — Develop ABAP Applications
댓글 0
아직 댓글이 없습니다.