ABAP SQL 튜닝 ④ - 접근 횟수를 줄여라

ABAP SQL 튜닝 4편 배너

ABAP SQL 튜닝 시리즈 네 번째 글입니다. 실무에서 가장 자주 만나는 성능 문제가 이 편에 있습니다.

루프 안의 SELECT

성능 리뷰에서 발견되면 거의 예외 없이 문제인 패턴입니다.

1" ✗ 주문 1만 건이면 DB에 1만 번 간다
2LOOP AT lt_order INTO DATA(ls_order).
3  SELECT SINGLE name1 FROM kna1
4    INTO @DATA(lv_name)
5    WHERE kunnr = @ls_order-kunnr.
6  ...
7ENDLOOP.

쿼리 하나하나는 빠릅니다. 인덱스도 탑니다. 그래서 더 위험합니다. ST05에서 개별 실행 시간만 보면 전혀 문제로 안 보입니다.

문제는 횟수입니다. 왕복 1건에 2ms라면 1만 건은 20초입니다. 이걸 N+1 문제라고 부릅니다.

TIP

ST05에서 SQL을 볼 때 동일한 쿼리가 반복 등장하는지를 먼저 확인하세요. 실행 시간 정렬만 보면 이 패턴은 절대 안 보입니다.

해결 1 - 미리 한 번에 읽고 메모리에서 찾기

가장 무난한 방법입니다.

1" ✓ 한 번에 읽어서 해시 테이블로 만든다
2DATA lt_cust TYPE HASHED TABLE OF ty_cust WITH UNIQUE KEY kunnr.
3
4SELECT kunnr, name1 FROM kna1
5  FOR ALL ENTRIES IN @lt_order
6  WHERE kunnr = @lt_order-kunnr
7  INTO TABLE @lt_cust.
8
9LOOP AT lt_order INTO DATA(ls_order2).
10  READ TABLE lt_cust INTO DATA(ls_cust)
11    WITH TABLE KEY kunnr = ls_order2-kunnr.
12  ...
13ENDLOOP.

DB 접근 1번 + 메모리 조회 N번이 됩니다. HASHED TABLE은 건수가 늘어도 조회 속도가 거의 일정해서 이 용도에 적합합니다.

해결 2 - JOIN으로 한 번에

애초에 두 테이블을 함께 읽으면 되는 경우입니다.

1SELECT o~vbeln, o~kunnr, c~name1
2  FROM zorder AS o
3  INNER JOIN kna1 AS c ON c~kunnr = o~kunnr
4  WHERE o~werks = @lv_werks
5  INTO TABLE @DATA(lt_joined).

FOR ALL ENTRIES의 함정

FOR ALL ENTRIES(이하 FAE)는 편리하지만, 모르고 쓰면 사고가 나는 함정이 몇 개 있습니다. 하나씩 짚겠습니다.

함정 1 - 빈 드라이버 테이블 (가장 위험)

!이렇게 동작한다드라이버가 비면 조건이 사라진다

lt_order가 비어 있으면 해당 WHERE 조건이 통째로 무시되어, 사실상 전건 조회가 됩니다.

반드시 이렇게쓰기 전에 비었는지 확인

FAE 앞에는 예외 없이 빈 테이블 체크를 답니다. 이건 선택이 아니라 규칙으로 삼는 편이 안전합니다.

1" ✗ 위험 - lt_order가 비면 kna1 전건을 읽는다
2SELECT kunnr, name1 FROM kna1
3  FOR ALL ENTRIES IN @lt_order
4  WHERE kunnr = @lt_order-kunnr
5  INTO TABLE @lt_cust.
6
7" ✓ 안전
8IF lt_order IS NOT INITIAL.
9  SELECT kunnr, name1 FROM kna1
10    FOR ALL ENTRIES IN @lt_order
11    WHERE kunnr = @lt_order-kunnr
12    INTO TABLE @lt_cust.
13ENDIF.
WARNING

이 함정은 평소엔 멀쩡하다가 특정 조건에서만 터집니다. 개발계 테스트 데이터로는 드라이버 테이블이 항상 채워져 문제가 안 보이다가, 운영에서 조회 결과가 0건인 날 갑자기 전건 조회가 나가는 식입니다.

함정 2 - 중복이 제거된다

FAE 결과에서는 선택한 필드 기준으로 중복 행이 제거됩니다. DISTINCT가 걸린 것처럼 동작합니다.

그래서 키 필드를 충분히 선택하지 않으면 행이 사라집니다.

1" ✗ posnr(품목)을 안 가져오면, 같은 vbeln의 여러 품목이 1건으로 합쳐진다
2SELECT vbeln, netwr FROM zsales
3  FOR ALL ENTRIES IN @lt_order
4  WHERE vbeln = @lt_order-vbeln
5  INTO TABLE @lt_item.
6
7" ✓ 키를 온전히 포함시킨다
8SELECT vbeln, posnr, netwr FROM zsales
9  FOR ALL ENTRIES IN @lt_order
10  WHERE vbeln = @lt_order-vbeln
11  INTO TABLE @lt_item2.

"건수가 왜 이것밖에 안 나오지?" 하는 상황의 상당수가 이 원인입니다.

함정 3 - 테이블 버퍼를 우회한다

FAE를 쓰면 SAP 테이블 버퍼를 타지 않고 DB로 직행합니다. 버퍼링된 소량 마스터 테이블을 FAE로 읽으면, 버퍼를 두고도 매번 DB에 가는 셈입니다.

소량 코드성 테이블이라면 FAE 대신 전체를 한 번 읽어 내부 테이블에 두고 쓰는 편이 나을 수 있습니다.

그 외 - 드라이버는 미리 정리하기

FAE는 드라이버 테이블 내용을 조건으로 만들어 DB에 보내는데, 여러 번에 나눠 보냅니다. 드라이버에 중복이 많으면 불필요한 작업이 늘어납니다.

1" 드라이버를 먼저 정리하면 효율이 올라간다
2SORT lt_order BY kunnr.
3DELETE ADJACENT DUPLICATES FROM lt_order COMPARING kunnr.
INFO

FAE가 한 번에 몇 건씩 묶어 보내는지는 시스템 프로파일 파라미터로 정해집니다. 이 값은 Basis 영역이므로, 개발자는 드라이버를 정리해 건수를 줄이는 쪽에 집중하면 됩니다.

FOR ALL ENTRIES vs JOIN

FOR ALL ENTRIES단계를 나눠 읽는다
  • 중간에 ABAP 로직을 끼울 수 있다
  • 드라이버가 이미 메모리에 있을 때 자연스럽다
  • 중복 제거·빈 테이블 함정을 늘 신경 써야 한다
  • 버퍼를 우회한다
JOIN한 번에 읽는다
  • DB 왕복이 1회
  • DB가 조인 최적화를 수행
  • 결과 행이 부풀 수 있어 조인 조건 설계가 중요
  • 버퍼를 우회한다

S/4HANA에서는 JOIN 쪽이 유리한 경우가 많습니다. 조인 처리를 DB에서 하는 것이 ⑦편에서 다룰 Code-to-Data 방향과도 맞습니다.

다만 무조건은 아닙니다. 드라이버 데이터가 이미 ABAP에 있고 중간 가공이 필요하다면 FAE가 자연스럽습니다. 판단이 서지 않으면 양쪽을 다 만들어 ST05로 재보세요.

테이블 버퍼링

접근 횟수를 줄이는 또 다른 축입니다. 자주 읽고 거의 안 바뀌는 테이블은 애플리케이션 서버 메모리에 버퍼링해 두면 DB에 가지 않아도 됩니다.

주로 코드성·설정성 소량 테이블이 대상입니다. 버퍼링 설정 자체는 테이블 기술 설정에서 하지만, 개발자가 알아야 할 것은 어떤 SQL이 버퍼를 무력화하는지입니다.

버퍼를 우회하는 대표 사례
FOR ALL ENTRIES
JOIN
집계 함수 (SUM, COUNT 등), GROUP BY
DISTINCT
기본키가 아닌 ORDER BY
서브쿼리
BYPASSING BUFFER 명시
WARNING

버퍼링은 데이터가 자주 바뀌는 테이블에는 쓰면 안 됩니다. 버퍼 무효화 비용이 이득보다 커지고, 서버 간 동기화 지연으로 잠깐 옛 데이터를 볼 수도 있습니다. 트랜잭션 테이블에는 부적합합니다.

정리

  • 루프 안의 SELECT가 1순위 점검 대상입니다. 개별로는 빠르기 때문에 눈에 잘 안 띕니다.
  • 해결은 보통 사전 일괄 조회 + 해시 테이블 또는 JOIN입니다.
  • FOR ALL ENTRIES 3대 함정을 기억하세요.
    1. 빈 드라이버 → 전건 조회 (반드시 체크)
    2. 중복 제거 → 키를 온전히 선택
    3. 버퍼 우회
  • 드라이버 테이블은 중복을 정리하고 넣으세요.
  • 버퍼링은 소량·변경 적은 테이블에만.

다음 ⑤편부터는 대량 조회를 두 편에 걸쳐 다룹니다. 먼저 조회 속도, 즉 페이징입니다.

Disclaimer — 이 포스트는 AI(Claude)를 활용하여 작성된 초안을 바탕으로 검수 및 보완하여 작성되었습니다. 내용 중 오류나 오타가 있다면 댓글로 알려주시면 감사하겠습니다.