1. Execution Plan을 못 읽으면 왜 큰일인가
SAP HANA는 인메모리 컬럼 스토어 덕분에 웬만한 쿼리는 빠르게 처리하지만, 데이터가 수억 건으로 늘어나거나 조인이 복잡해지면 "HANA인데 왜 느리지?"라는 상황이 반드시 옵니다. 이때 감으로 인덱스를 추가하거나 힌트를 남발하는 것은 도박에 가깝습니다. Execution Plan은 옵티마이저가 쿼리를 어떤 연산자 트리로 실행할지 보여주는 설계도이며, 이를 읽을 수 있어야 병목의 원인을 데이터로 증명하고 고칠 수 있습니다. 이 글을 끝까지 따라오면 다음을 할 수 있게 됩니다.
- EXPLAIN PLAN 구문으로 예상 실행 계획을 추출하고 해석
- COLUMN SEARCH, JOIN, AGGREGATION 등 주요 노드의 의미 파악
- SUBTREE_COST와 OUTPUT_SIZE로 비용 병목 지점 식별
- 인덱스·파티션 프루닝이 Plan에 반영됐는지 검증
- 실무에서 자주 만나는 안티패턴 3가지 회피
2. 시작 전에 갖춰야 할 것 — 환경과 버전
이 글의 예제는 SAP HANA 2.0 SPS 05 이상과 SAP HANA Cloud를 기준으로 합니다. 도구는 SAP HANA Database Explorer(HANA Cloud 권장) 또는 SAP HANA Studio의 SQL Console 어느 쪽이든 무방합니다. SQL의 SELECT 권한 외에, 예상 계획 조회를 위해 SYS.EXPLAIN_PLAN_TABLE 접근이 가능해야 하고, 실측 분석까지 하려면 PlanViz(Plan Visualizer) 실행 권한이 필요합니다. 기본적인 SQL 조인·집계 문법과 컬럼 스토어/로우 스토어의 차이를 알고 있다면 이해가 훨씬 빠릅니다. HANA 2.0 SPS 04 이후에는 HEX(HANA Execution Engine)라는 통합 엔진이 점진적으로 도입되었고 HANA Cloud에서는 HEX가 기본 경로로 자주 등장하므로, 온프레미스와 클라우드의 Plan 출력이 다를 수 있다는 점을 미리 기억해 두세요.
3. Explain Plan 실행하는 법 — 기본 예제
HANA에서 예상 실행 계획은 EXPLAIN PLAN ... FOR 구문으로 생성하고, 결과는 SYS.EXPLAIN_PLAN_TABLE에서 조회합니다. 판매 오더 헤더 테이블을 예로 들어 보겠습니다.
-- 예상 실행 계획 생성 (실제 쿼리는 실행되지 않음)
EXPLAIN PLAN SET STATEMENT_NAME = 'SO_HEADER_LOOKUP' FOR
SELECT so.SALES_ORDER_ID, so.ORDER_DATE, so.NET_AMOUNT
FROM SALES_ORDER_HEADER AS so
WHERE so.CUSTOMER_ID = 'C-10023'
AND so.ORDER_DATE >= '2026-01-01';
-- 결과 조회
SELECT OPERATOR_ID, OPERATOR_NAME, OPERATOR_DETAILS,
EXECUTION_ENGINE, TABLE_NAME, OUTPUT_SIZE, SUBTREE_COST
FROM SYS.EXPLAIN_PLAN_TABLE
WHERE STATEMENT_NAME = 'SO_HEADER_LOOKUP'
ORDER BY OPERATOR_ID;
-- 분석이 끝나면 정리
DELETE FROM SYS.EXPLAIN_PLAN_TABLE
WHERE STATEMENT_NAME = 'SO_HEADER_LOOKUP';
이미 Plan Cache에 올라간 쿼리는 M_SQL_PLAN_CACHE에서 PLAN_ID를 찾아 EXPLAIN PLAN FOR SQL PLAN CACHE ENTRY <plan_id>로 캐시된 계획 그대로를 확인할 수도 있습니다. 바인드 변수가 섞인 운영 쿼리를 분석할 때 특히 유용한 방식입니다.
4. 주요 노드와 비용 수치 해석 — 핵심 개념
Plan은 트리 구조이고, 자식 노드의 결과가 부모 노드로 흘러 올라갑니다. 공장 조립 라인에 비유하면 맨 아래 노드가 원자재 창고(테이블 접근), 중간 노드가 조립 공정(조인·집계), 맨 위가 출하장(최종 결과)입니다.
| 노드 | 의미 | 주목할 점 |
|---|---|---|
| COLUMN SEARCH | 컬럼 엔진이 처리하는 연산 블록 | 블록이 잘게 쪼개지면 중간 결과 물질화 비용 증가 |
| ROW SEARCH | 로우 엔진 처리 구간 | 컬럼→로우 전환이 잦으면 성능 저하 신호 |
| COLUMN TABLE | 컬럼 테이블 접근 | OPERATOR_DETAILS의 FILTER 조건 푸시다운 여부 확인 |
| HASH JOIN | 해시 기반 조인 방식 | 대용량 조인에 적합, 빌드 사이드 크기 확인 필수 |
| NESTED LOOP JOIN | 중첩 루프 조인 | 대용량+NESTED LOOP 조합은 위험 신호 |
| AGGREGATION | GROUP BY 집계 | 입력 행 수(OUTPUT_SIZE)가 과도한지 확인 |
OUTPUT_SIZE는 해당 노드가 내보낼 것으로 추정되는 행 수, SUBTREE_COST는 해당 노드와 하위 트리 전체의 상대적 예상 비용입니다. 중요한 것은 이 비용이 밀리초가 아니라 옵티마이저 내부의 상대값이라는 점입니다. 절대치보다 "전체 비용 중 어느 서브트리가 큰 비중을 차지하는가"를 보는 것이 올바른 독법입니다.
5. HASH JOIN vs NESTED LOOP — Plan에서 차이 읽기
같은 조인 쿼리라도 옵티마이저가 선택하는 방식에 따라 성능이 크게 달라집니다. 헤더·아이템 두 테이블 조인을 예로 Plan 차이를 비교합니다.
EXPLAIN PLAN SET STATEMENT_NAME = 'SO_MONTHLY_REV' FOR
SELECT so.CUSTOMER_ID,
SUBSTRING(TO_VARCHAR(so.ORDER_DATE, 'YYYYMMDD'), 1, 6) AS ORDER_MONTH,
SUM(item.NET_VALUE) AS TOTAL_REV
FROM SALES_ORDER_HEADER AS so
INNER JOIN SALES_ORDER_ITEM AS item
ON item.SALES_ORDER_ID = so.SALES_ORDER_ID
WHERE so.SALES_ORG = 'KR01'
AND so.ORDER_DATE BETWEEN '2026-01-01' AND '2026-06-30'
GROUP BY so.CUSTOMER_ID,
SUBSTRING(TO_VARCHAR(so.ORDER_DATE, 'YYYYMMDD'), 1, 6);
SELECT LPAD(' ', LEVEL) || OPERATOR_NAME AS PLAN_TREE,
EXECUTION_ENGINE, OUTPUT_SIZE, SUBTREE_COST, OPERATOR_DETAILS
FROM SYS.EXPLAIN_PLAN_TABLE
WHERE STATEMENT_NAME = 'SO_MONTHLY_REV'
ORDER BY OPERATOR_ID;
Plan을 읽는 5단계 순서입니다. 1단계: 테이블 접근 노드에서 FILTER CONDITION이 푸시다운됐는지 확인합니다. WHERE 조건이 스캔 단계에서 처리되어야 중간 행 수를 줄일 수 있습니다. 2단계: 각 접근 노드의 OUTPUT_SIZE가 실제 데이터 분포와 크게 어긋나지 않는지 봅니다. 통계가 낡으면 조인 순서 자체가 틀어집니다. 3단계: JOIN 노드 방식을 확인합니다. 대량 데이터에 HASH JOIN이면 무난하나, NESTED LOOP면 선택도 낮은 조건 또는 인덱스 누락을 의심합니다. 4단계: SUBTREE_COST가 급격히 커지는 노드를 찾아 병목 후보를 좁힙니다. 5단계: PlanViz로 실측 시간·행 수를 교차 검증해 예상치(EXPLAIN)와 실측치의 10배 이상 괴리를 확인하면 통계 갱신이나 쿼리 재작성을 검토합니다.
6. 인덱스·파티션 튜닝 전후 Plan 비교
컬럼 테이블은 기본적으로 전 컬럼 압축·정렬 구조라 인덱스 없이도 스캔이 빠르지만, 선택도가 매우 높은 등가 조건이 초당 수백 번 반복되는 패턴에서는 인버티드 인덱스가 효과적입니다.
-- 반복 조회 패턴에 대한 인버티드 인덱스 생성
CREATE INDEX IDX_SO_CUSTOMER
ON SALES_ORDER_HEADER (CUSTOMER_ID);
-- 월 단위 레인지 파티션으로 재구성
ALTER TABLE SALES_ORDER_HEADER
PARTITION BY RANGE (ORDER_DATE)
( PARTITION '2026-01-01' <= VALUES < '2026-04-01',
PARTITION '2026-04-01' <= VALUES < '2026-07-01',
PARTITION OTHERS );
튜닝 후 같은 EXPLAIN PLAN을 다시 떠서 비교합니다. 인덱스가 실제로 쓰이면 OPERATOR_DETAILS에 인덱스 활용이 표시되고 SUBTREE_COST가 눈에 띄게 줄어듭니다. 파티션 테이블에서 WHERE 절에 파티션 키(ORDER_DATE) 조건이 있으면 파티션 프루닝이 수행되어 접근 파티션 수가 줄어든 것을 Plan에서 확인할 수 있습니다.
7. 자주 보는 안티패턴 3가지
첫째, 암시적 형 변환. VARCHAR 컬럼을 숫자 리터럴과 비교하면(WHERE SALES_ORDER_ID = 30001234) 전 행 변환이 일어나 필터 푸시다운과 인덱스 활용이 모두 무력화됩니다. 리터럴 타입을 컬럼에 맞추세요. 둘째, 엔진 혼용 유발. 컬럼 엔진이 처리하기 어려운 함수나 상관 서브쿼리를 남용하면 COLUMN SEARCH가 여러 블록으로 쪼개지고 ROW SEARCH가 끼어들어 중간 결과 물질화 비용이 커집니다. 셋째, SELECT * 습관. 컬럼 스토어는 필요한 컬럼만 읽을 때 이점이 극대화되는데, 전 컬럼 조회는 이 장점을 스스로 버리는 것입니다.
- Q. EXPLAIN_PLAN_TABLE을 조회했는데 비어 있습니다. A. 계획 생성과 조회는 같은 세션·같은 사용자로 해야 하며, STATEMENT_NAME 오타나 이전 DELETE 누락이 흔한 원인입니다.
- Q. SUBTREE_COST가 작은데도 쿼리가 느립니다. A. 비용은 추정 상대값입니다. 통계가 낡았거나 바인드 변수 분포가 치우친 경우 추정이 빗나가므로 PlanViz 실측과 반드시 교차 검증하세요.
- Q. 인덱스를 만들었는데 Plan에 안 보입니다. A. 조건의 선택도가 낮거나 형 변환·함수 래핑으로 인덱스 적용이 불가한 경우입니다. 조건식을 컬럼 원형 그대로 두도록 재작성하세요.
8. 이어서 볼 주제 — Plan 분석 다음 단계
Explain Plan으로 예상 계획을 읽는 감이 잡혔다면, 다음 여정은 PlanViz로 노드별 실측 시간·스레드 분포를 분석하는 것, M_SQL_PLAN_CACHE·M_EXPENSIVE_STATEMENTS 기반의 상시 모니터링 체계 구축, SQL 힌트를 통한 조인 방식 제어, 그리고 HEX 엔진 동작 특성 이해입니다. Calculation View가 섞인 쿼리라면 뷰 언폴딩 여부가 Plan에 어떻게 나타나는지도 함께 살펴보길 권장합니다.
댓글 0
아직 댓글이 없습니다.