이 글에서 다루는 내용
SAP HANA 기반 시스템에서 SELECT * 구문은 개발 초기에는 편리해 보이지만, 프로덕션 환경에 그대로 배포되면 다양한 성능·유지보수 문제를 일으킵니다. 이 글에서는 SAP HANA Cloud(QRC 2/2024 기준) 및 HANA 2.0 SPS07 환경을 기준으로, 컬럼스토어 엔진의 동작 방식과 함께 SELECT *가 왜 위험한지를 구체적인 B2B 비즈니스 시나리오(SalesOrder, Product, Invoice)로 풀어봅니다.
- HANA 컬럼스토어에서
SELECT *가 발생시키는 I/O·메모리 비용 이해 - 인덱스 및 프로젝션 푸시다운이 작동하지 않는 패턴 식별
- 실무에서 안전하게 컬럼을 명시하는 패턴과 ABAP CDS/Open SQL 적용
- 스키마 변경(컬럼 추가/순서 변경)에 강건한 코드 작성법
읽기 전에 알고 있으면 좋은 것
SQL 기본 문법(SELECT, JOIN, WHERE)과 관계형 데이터 모델 개념을 알고 있어야 합니다. SAP HANA의 행/열 저장 구조와 ABAP Open SQL의 기초적인 사용 경험이 있으면 이해가 빠릅니다. CDS View나 AMDP에 대한 사전 지식은 필수는 아니지만, 후반부 실전 예제에서 도움이 됩니다.
환경 및 준비물
아래 예제는 다음 환경을 기준으로 작성되었습니다. 다른 버전에서도 동일한 원리가 적용되지만, 옵티마이저 동작 디테일은 릴리스에 따라 달라질 수 있습니다.
- SAP HANA Cloud QRC 2/2024 또는 SAP HANA 2.0 SPS07
- SAP BTP, ABAP Environment 2402 또는 S/4HANA 2023
- SAP HANA Database Explorer 또는 DBeaver 24.x
- SQL 실행 권한(SELECT 권한)이 있는 스키마
- 측정용 샘플 테이블:
ZSALES_ORDER(약 1천만 행, 컬럼 35개 가정)
실습용 테스트 데이터는 자체 생성하거나, 사내 개발 시스템의 비식별 데이터를 사용하는 것을 권장합니다. 프로덕션 시스템에서 직접 EXPLAIN PLAN을 돌리는 행위는 일반적으로 워크로드에 영향을 줄 수 있으므로 피하는 것이 좋습니다.
핵심 개념: 왜 HANA에서 SELECT *가 더 위험한가
전통적인 행 기반(Row Store) RDBMS에서는 한 행 전체가 디스크 페이지에 함께 저장됩니다. 즉, 하나의 컬럼만 읽든 모든 컬럼을 읽든 디스크 I/O 비용 차이가 크지 않은 경우가 많습니다. 그러나 SAP HANA의 핵심 엔진인 컬럼스토어(Column Store)는 각 컬럼을 별도의 메모리 영역에 압축된 형태로 저장합니다. 도서관에 비유하자면, 행 기반은 한 권의 책에 모든 정보가 묶여 있는 구조이고, 컬럼스토어는 책의 각 챕터(컬럼)가 서로 다른 책장에 분리 보관된 구조입니다.
컬럼스토어에서 SELECT col1, col2 FROM T를 실행하면 엔진은 정확히 두 컬럼 벡터만 메모리에서 스캔합니다. 반면 SELECT * FROM T는 모든 컬럼 벡터를 메모리로 끌어오고, 사전(Dictionary) 디코딩을 수행하며, 결과 셋을 재조립합니다. 컬럼이 35개라면 단순 산술로도 약 17배의 작업량이 발생할 수 있습니다.
또한 HANA 옵티마이저는 컬럼 프루닝(Column Pruning)과 프로젝션 푸시다운(Projection Pushdown)을 통해 필요한 컬럼만 상위 연산자로 전달하는 최적화를 수행합니다. SELECT *는 이러한 푸시다운 기회를 원천적으로 차단합니다. Index-Only Scan, Concat Index 활용, 캐시된 결과 재사용 등도 마찬가지로 어려워집니다.
네트워크 계층의 비용도 무시할 수 없습니다. 애플리케이션 서버(ABAP/Java)와 DB 사이에 흐르는 데이터 양이 늘어나면 RTT(Round-Trip Time)는 동일해도 전송 대역폭이 병목이 됩니다. 모바일 UI나 Fiori 앱처럼 사용자가 즉시 응답을 기대하는 시나리오에서 체감 성능에 직접 영향을 줍니다.
마지막으로 유지보수 측면에서, SELECT *는 코드 리뷰어가 "이 쿼리가 실제로 어떤 컬럼에 의존하는가"를 한눈에 파악하기 어렵게 만듭니다. 컬럼이 추가/이름 변경되면 ORM이나 결과 매핑이 조용히 깨질 수 있습니다.
1단계 예제: 기본 비교 — SELECT * vs 명시적 컬럼
가장 단순한 영업주문 헤더 테이블을 가정합니다. 35개 컬럼 중 화면에 보여줄 4개만 필요한 상황입니다.
-- 안티패턴: 필요한 4개를 위해 35개를 모두 읽음
SELECT *
FROM ZSALES_ORDER
WHERE SALES_ORG = '1010'
AND CREATED_AT >= ADD_DAYS(CURRENT_DATE, -7);
-- 권장 패턴: 필요한 컬럼만 명시
SELECT SO_NUMBER,
CUSTOMER_ID,
NET_AMOUNT,
CURRENCY_CODE
FROM ZSALES_ORDER
WHERE SALES_ORG = '1010'
AND CREATED_AT >= ADD_DAYS(CURRENT_DATE, -7);
EXPLAIN PLAN으로 두 쿼리를 비교하면 일반적으로 두 번째 쿼리의 추정 비용과 메모리 사용량이 크게 낮게 나옵니다. 특히 큰 LOB(NCLOB, BLOB) 컬럼이 테이블에 포함되어 있다면 차이는 더욱 극적으로 벌어집니다.
2단계 예제: 실무 시나리오 — JOIN, 로깅, 에러 처리
실무에서는 주문, 제품, 인보이스 테이블을 조인해 리포팅 화면에 데이터를 공급하는 경우가 많습니다. 아래는 ABAP Open SQL로 작성한 예시입니다.
" 안티패턴: 모든 컬럼을 가져온 뒤 ABAP 내부에서 일부만 사용
DATA: lt_orders TYPE STANDARD TABLE OF zsales_order.
TRY.
SELECT *
FROM zsales_order
INTO TABLE @lt_orders
WHERE sales_org = @p_sales_org.
CATCH cx_sy_open_sql_db INTO DATA(lx_db).
" 컬럼이 35개 → 네트워크 페이로드 비대화
" 신규 컬럼이 추가되어도 인지하지 못함
MESSAGE lx_db->get_text( ) TYPE 'E'.
ENDTRY.
" 권장 패턴: 명시적 컬럼 + 전용 구조체
TYPES: BEGIN OF ty_order_summary,
so_number TYPE zsales_order-so_number,
customer_id TYPE zsales_order-customer_id,
product_name TYPE zproduct-product_name,
net_amount TYPE zsales_order-net_amount,
currency TYPE zsales_order-currency_code,
invoice_no TYPE zinvoice-invoice_no,
END OF ty_order_summary.
DATA: lt_summary TYPE STANDARD TABLE OF ty_order_summary.
TRY.
SELECT so~so_number,
so~customer_id,
pr~product_name,
so~net_amount,
so~currency_code AS currency,
inv~invoice_no
FROM zsales_order AS so
INNER JOIN zproduct AS pr
ON pr~product_id = so~product_id
LEFT OUTER JOIN zinvoice AS inv
ON inv~so_number = so~so_number
INTO TABLE @lt_summary
WHERE so~sales_org = @p_sales_org
AND so~created_at >= @lv_from_date.
cl_logger=>info( |Loaded { lines( lt_summary ) } rows for org { p_sales_org }| ).
CATCH cx_sy_open_sql_db INTO DATA(lx_db).
cl_logger=>error( lx_db->get_text( ) ).
RAISE EXCEPTION TYPE zcx_order_read
EXPORTING previous = lx_db.
ENDTRY.
3단계 예제: 프로덕션 — CDS View, 인덱스, 단위 테스트
대규모 시스템에서는 SQL을 코드 곳곳에 흩뿌리는 대신, CDS View로 데이터 모델을 단일 진실 공급원(Single Source of Truth)으로 정리하는 것이 권장됩니다. 동시에 적절한 인덱스와 테스트가 함께 갖춰져야 합니다.
@AbapCatalog.sqlViewName: 'ZVSO_SUMMARY'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Summary for Reporting'
define view Z_C_SalesOrderSummary
as select from zsales_order as so
inner join zproduct as pr on pr.product_id = so.product_id
left outer join zinvoice as inv on inv.so_number = so.so_number
{
key so.so_number,
so.customer_id,
pr.product_name,
so.net_amount,
so.currency_code as currency,
inv.invoice_no,
so.created_at,
so.sales_org
}
where so.deletion_flag = ' ';
CREATE INDEX IDX_SO_ORG_DATE
ON ZSALES_ORDER (SALES_ORG, CREATED_AT)
ASYNCHRONOUS;
SELECT so_number, customer_id, net_amount, currency
FROM z_c_salesordersummary
INTO TABLE @DATA(lt_report)
WHERE sales_org = @p_org
AND created_at IN @r_period
ORDER BY so_number
UP TO 500 ROWS.
METHOD test_summary_columns.
DATA(lt_data) = z_cl_order_reader=>read( i_sales_org = '1010' ).
cl_abap_unit_assert=>assert_not_initial( lt_data ).
cl_abap_unit_assert=>assert_equals(
exp = 'EUR'
act = lt_data[ 1 ]-currency ).
ENDMETHOD.
현장에서 자주 겪는 문제와 해결
Q1. "내부 테이블 구조와 동일하니 SELECT *가 더 빠르지 않나요?"
ABAP 내부 테이블이 DB 테이블 구조와 동일하더라도, HANA 컬럼스토어에서 모든 컬럼을 읽는 비용은 그대로 발생합니다. INTO CORRESPONDING FIELDS 대신 명시적 SELECT가 가장 빠른 경우가 일반적입니다.
Q2. "CDS View에서 SELECT *를 써도 되나요?"
CDS View 정의 자체에는 *가 허용되지 않습니다. 호출 시 SELECT *는 문법적으로는 가능하지만, view의 모든 필드를 끌어오게 되므로 동일한 안티패턴이 됩니다.
Q3. "EXPLAIN PLAN에서 차이가 별로 안 보이는데요?"
행 수가 적거나 캐시가 충분히 워밍업된 경우 차이가 작아 보일 수 있습니다. PlanViz 또는 M_SQL_PLAN_CACHE를 통해 실측 메모리(MAIN_MEMORY_SIZE)와 응답 시간 분포를 함께 보는 것이 정확합니다.
흔한 실수 정리
- "나중에 컬럼이 추가되면 자동으로 반영되니 편리하다"는 오해 — 실제로는 매핑 오류, 직렬화 깨짐, 보안 노출(민감 컬럼 추가 시) 위험이 증가합니다.
- EXISTS 대신
SELECT * ... LIMIT 1로 존재 여부만 확인하는 패턴 —SELECT 1또는 ABAP의SELECT SINGLE @abap_true를 사용하는 것이 권장됩니다. - 서브쿼리 안의
SELECT *— 가독성과 의도 전달 측면에서 명시적 컬럼이 낫습니다.
이어서 살펴보면 좋은 주제
- HANA PlanViz를 활용한 쿼리 병목 분석과 컬럼 프루닝 검증
- Code Pushdown 원칙과 AMDP(ABAP Managed Database Procedure) 설계
- CDS View의 어노테이션 기반 캐시 활용
- BTP, ABAP Environment에서의 RAP 모델과 쿼리 최적화
- HANA Cloud의 NSE(Native Storage Extension) 워밍업 전략
댓글 0
아직 댓글이 없습니다.