이 글이 답하는 질문
ST05 트레이스를 열어보면 느린 SELECT의 범인은 대부분 몇 가지 패턴으로 압축됩니다. 이 글은 다음 질문에 답합니다.
- FOR ALL ENTRIES는 왜 위험하고, 언제 INNER JOIN으로 바꿔야 하나?
- LOOP 안 SELECT(N+1)를 한 방 쿼리로 바꾸는 방법은?
- 인덱스가 있는데도 풀스캔이 뜨는 이유는?
- 버퍼링·집계 푸시다운·존재 확인은 어떻게 처리해야 빠른가?
사전 가정
예제는 ABAP 7.50 이상(S/4HANA 및 NetWeaver 7.5x)의 신규 Open SQL 문법(@ 호스트 변수, 인라인 선언, 콤마 구분 필드 리스트)을 사용합니다. 7.40 미만 시스템이라면 구문만 구형으로 바꾸면 원리는 동일합니다. 테이블명은 실무 시나리오를 흉내 낸 자체 창작 이름(zt_sales_hdr 판매오더 헤더, zt_sales_itm 아이템, zt_matmaster 자재 마스터)입니다.
패턴 1. FOR ALL ENTRIES 함정 → INNER JOIN
" 문제: lt_orders가 비어 있으면 WHERE가 통째로 무시된다
SELECT vbeln, posnr, matnr, kwmeng
FROM zt_sales_itm
FOR ALL ENTRIES IN @lt_orders
WHERE vbeln = @lt_orders-vbeln
INTO TABLE @DATA(lt_items).
" 개선: JOIN으로 DB가 한 번에 처리하게 한다
SELECT h~vbeln, i~posnr, i~matnr, i~kwmeng
FROM zt_sales_hdr AS h
INNER JOIN zt_sales_itm AS i ON h~vbeln = i~vbeln
WHERE h~erdat >= @lv_from
INTO TABLE @DATA(lt_items).
왜 빠른가: FOR ALL ENTRIES는 드라이버 테이블이 비어 있으면 해당 조건이 제거되어 전건 조회가 됩니다. 또 드라이버 테이블의 중복을 미리 제거하지 않으면 같은 조건이 반복 전송되고, 내부적으로 블로킹 팩터 단위로 쿼리가 여러 번 쪼개져 나갑니다. 결과에 암묵적 DISTINCT가 적용되어 중복 행이 사라지는 부작용도 있습니다. JOIN은 옵티마이저가 실행 계획을 한 번만 세우고 단일 왕복으로 끝냅니다. 부득이 FAE를 쓴다면 IF lt_orders IS NOT INITIAL 체크와 SORT + DELETE ADJACENT DUPLICATES는 필수입니다.
패턴 2. LOOP 안 SELECT(N+1) → 한 방 쿼리 + 내부테이블 가공
" 문제: 아이템 1만 건이면 DB 왕복도 1만 번
LOOP AT lt_items INTO DATA(ls_item).
SELECT SINGLE maktx FROM zt_matmaster
WHERE matnr = @ls_item-matnr
INTO @ls_item-maktx.
MODIFY lt_items FROM ls_item.
ENDLOOP.
" 개선: 자재명을 한 번에 읽어 해시 테이블로 매핑
SELECT matnr, maktx FROM zt_matmaster
WHERE matnr IN @lr_matnr
INTO TABLE @DATA(lt_mat).
DATA(lt_map) = CORRESPONDING zty_mat_hash( lt_mat ). " HASHED, KEY matnr
LOOP AT lt_items ASSIGNING FIELD-SYMBOL(<fs>).
<fs>-maktx = VALUE #( lt_map[ matnr = <fs>-matnr ]-maktx OPTIONAL ).
ENDLOOP.
왜 빠른가: DB 왕복 비용은 실제 데이터 읽기보다 네트워크·파싱 오버헤드가 지배적입니다. N번의 SELECT SINGLE을 1번의 SELECT로 줄이고, 애플리케이션 서버에서는 해시 키 조회(O(1))로 붙이는 편이 일반적으로 수십 배 빠릅니다.
패턴 3. SELECT * → 필드 리스트 명시
" 문제: 3개 필드만 쓰면서 100개 컬럼을 다 끌어온다
SELECT * FROM zt_sales_hdr
WHERE erdat >= @lv_from
INTO TABLE @DATA(lt_hdr).
" 개선
SELECT vbeln, kunnr, netwr
FROM zt_sales_hdr
WHERE erdat >= @lv_from
INTO TABLE @DATA(lt_hdr).
왜 빠른가: HANA는 컬럼 스토어라 컬럼 단위로 데이터를 읽습니다. 필드 리스트를 명시하면 필요한 컬럼만 접근하므로 I/O와 네트워크 전송량이 그만큼 줄어듭니다. 로우 스토어 DB에서도 전송량 절감 효과는 유효하며, 구조 변경 시 의도가 명확해지는 유지보수 이점도 있습니다.
패턴 4. WHERE 절과 인덱스 — 선두 컬럼 원칙
" 인덱스: (mandt, kunnr, erdat) 라고 가정
" 문제 1: 선두 컬럼 kunnr 없이 erdat만 → 인덱스 레인지 스캔 불가
SELECT vbeln FROM zt_sales_hdr
WHERE erdat = @lv_date INTO TABLE @DATA(lt1).
" 문제 2: 부정 조건과 선행 와일드카드 → 인덱스 무력화
SELECT vbeln FROM zt_sales_hdr
WHERE kunnr <> @lv_kunnr AND ernam LIKE '%KIM'
INTO TABLE @DATA(lt2).
" 개선: 선두 컬럼을 등호/IN으로 고정하고 긍정 조건으로 재구성
SELECT vbeln FROM zt_sales_hdr
WHERE kunnr IN @lr_kunnr
AND erdat = @lv_date
INTO TABLE @DATA(lt3).
왜 빠른가: 인덱스는 선두 컬럼부터 정렬된 구조라, 선두 컬럼 조건이 빠지면 범위를 좁힐 수 없습니다. <>·NOT은 "어디에 없는지"를 인덱스로 찾을 수 없고, LIKE '%..'는 시작점을 특정할 수 없어 일반적으로 풀스캔으로 떨어집니다. HANA에서는 전통적 세컨더리 인덱스 의존도가 낮아지지만, 선택도 높은 WHERE 조건을 만드는 원칙 자체는 그대로 유효합니다.
패턴 5. 테이블 버퍼링과 BYPASSING BUFFER 오남용
" 문제: 완전 버퍼링된 설정 테이블을 매번 DB까지 다녀온다
SELECT SINGLE low_val, high_val
FROM zt_config
BYPASSING BUFFER
WHERE param = @lv_param
INTO @DATA(ls_cfg).
" 개선: 버퍼를 그대로 태운다
SELECT SINGLE low_val, high_val
FROM zt_config
WHERE param = @lv_param
INTO @DATA(ls_cfg).
왜 빠른가: 버퍼링된 테이블은 애플리케이션 서버 공유 메모리에서 읽으므로 DB 왕복이 없습니다. BYPASSING BUFFER는 방금 다른 서버가 갱신한 값을 즉시 읽어야 하는 특수한 경우에만 쓰는 것이 권장됩니다. 반대로 주의할 점도 있습니다. 집계 함수, JOIN, 서브쿼리, 기본 키 순서가 아닌 ORDER BY 등은 버퍼를 암묵적으로 우회하므로, 버퍼링 테이블이라고 안심하다가 트레이스에서 DB 액세스를 발견하는 일이 흔합니다.
패턴 6. 집계는 DB로 — SUM/COUNT/GROUP BY 푸시다운
" 문제: 수백만 행을 끌어와 앱서버 루프에서 합산
SELECT matnr, kwmeng FROM zt_sales_itm
WHERE erdat >= @lv_from INTO TABLE @DATA(lt_all).
LOOP AT lt_all INTO DATA(ls).
COLLECT ls INTO lt_sum.
ENDLOOP.
" 개선: 집계를 DB에 맡기고 결과만 받는다
SELECT matnr, SUM( kwmeng ) AS total_qty
FROM zt_sales_itm
WHERE erdat >= @lv_from
GROUP BY matnr
INTO TABLE @DATA(lt_sum).
왜 빠른가: S/4HANA에서 강조되는 코드-투-데이터 원칙 그대로입니다. 수백만 행을 네트워크로 옮겨 루프 도는 대신, 컬럼 스토어에 최적화된 DB 집계 엔진이 처리하고 결과 몇백 행만 전송합니다. 전송량과 앱서버 메모리 사용이 동시에 줄어듭니다.
패턴 7. 존재 확인에 전건 SELECT 금지 — UP TO 1 ROWS
" 문제: 있는지만 보면 되는데 전건을 메모리에 올린다
SELECT vbeln FROM zt_sales_hdr
WHERE kunnr = @lv_kunnr INTO TABLE @DATA(lt_chk).
IF lines( lt_chk ) > 0. "...
ENDIF.
" 개선: 첫 행에서 멈춘다 (7.50+에서는 상수 리터럴도 가능)
SELECT SINGLE @abap_true FROM zt_sales_hdr
WHERE kunnr = @lv_kunnr
INTO @DATA(lv_exists).
IF lv_exists = abap_true. "...
ENDIF.
왜 빠른가: UP TO 1 ROWS나 SELECT SINGLE은 DB가 첫 매칭 행을 찾는 즉시 탐색을 중단합니다. 인덱스만 스치고 끝나는 경우가 많아 전건 조회 대비 비용이 극적으로 낮습니다. 상위 n건 미리보기 화면에도 UP TO @lv_rows ROWS와 ORDER BY 조합이 적합합니다.
삽질 노트
실제로 겪은 사례 세 가지를 FAQ 형태로 남깁니다.
- Q. FAE 프로그램이 평소엔 멀쩡한데 가끔 몇 분씩 걸립니다. A. 선행 SELECT가 0건을 반환하는 날, 드라이버 테이블이 비어 2천만 행 테이블 전건 조회가 발생한 사례가 있었습니다. ST05에서 WHERE 절이 사라진 SQL을 확인하고 INITIAL 체크로 잡았습니다.
- Q. 인덱스를 새로 만들었는데 여전히 느립니다. A. WHERE 절에 선두 컬럼이 빠져 있거나, 값 분포가 한쪽으로 쏠려 옵티마이저가 인덱스를 버리는 경우가 대부분입니다. 실행 계획(Explain)을 먼저 확인하는 습관이 권장됩니다.
- Q. 버퍼링 테이블인데 트레이스에 DB 액세스가 찍힙니다. A. 해당 SELECT에 JOIN이나 집계가 섞여 버퍼가 암묵 우회된 경우였습니다. 버퍼를 태우려면 단순 키 조회 형태로 문장을 분리해야 합니다.
핵심 한 줄
DB가 잘하는 일(필터·조인·집계)은 DB에 맡기고, 애플리케이션 서버로는 정말 필요한 행과 컬럼만 가져와라.
더 파볼 주제
이번 7가지 패턴이 몸에 익었다면, CDS 뷰로 로직 자체를 DB 계층에 내리는 방법과 AMDP, 그리고 SQL Monitor(SQLM)·ST05 기반의 상시 성능 모니터링으로 확장해 보세요. 아래 자료가 출발점으로 유용합니다.
댓글 0
아직 댓글이 없습니다.