SAP HANA SQLScript 성능 최적화 — Procedure 설계, Table Variable, Parallel Execution 실전 가이드



1. 왜 SQLScript 성능 최적화가 중요한가
SAP HANA는 인메모리 컬럼 스토어 데이터베이스로서 대용량 데이터를 빠르게 처리할 수 있도록 설계되었습니다. 하지만 SQLScript로 작성된 Procedure가 비효율적이면, HANA의 병렬 처리 엔진이 제 역할을 하지 못하고 단일 스레드에서 순차적으로 실행되는 상황이 벌어집니다.
일반적으로 성능 문제가 발생하는 패턴은 다음과 같습니다.
- 커서(Cursor) 기반 반복 처리 — 행 단위 로직은 HANA의 컬럼 스토어 장점을 무력화합니다.
- 동적 SQL(Dynamic SQL) 남용 — 옵티마이저가 실행 계획을 미리 준비하지 못합니다.
- 불필요한 데이터 이동 — 애플리케이션 레이어에서 처리할 로직을 DB로 내리지 않거나, 반대로 DB 결과를 불필요하게 올리는 경우입니다.
- Table Variable 미활용 — 임시 테이블 대신 Table Variable을 쓰면 SQL 레이어 호출 오버헤드를 줄일 수 있습니다.
SAP HANA Performance Developer Guide에서는 "Code Pushdown" 원칙을 강조합니다. 데이터 중심 로직을 가능한 한 데이터베이스 레벨에서 실행하여 네트워크 오버헤드와 컨텍스트 전환을 최소화하라는 것입니다. 이 튜토리얼에서는 Procedure 설계부터 병렬 실행, 성능 분석 도구까지 단계별로 살펴봅니다.
학습 목표 체크리스트
- SQLScript Procedure의 올바른 구조와 READS SQL DATA 옵션을 이해한다
- 선언형 패턴과 명령형 패턴의 성능 차이를 구분한다
- Table Variable Operator를 활용해 중간 결과를 효율적으로 처리한다
- Parallel Execution 힌트를 적용하고 주의사항을 파악한다
- PlanViz와 Expensive Statement Trace로 병목 구간을 식별한다
2. SQLScript 기본 구조 — Procedure, Table Variable, READS SQL DATA
선수 지식
이 튜토리얼을 따라가려면 SQL 기본 문법(SELECT, JOIN, GROUP BY)에 대한 이해와 SAP HANA Studio 또는 SAP Business Application Studio 사용 경험이 필요합니다. ABAP 경험이 있으면 AMDP 섹션에서 도움이 되지만, 필수는 아닙니다.
환경 및 버전
- SAP HANA: 2.0 SPS 06 이상 또는 SAP HANA Cloud
- 개발 도구: SAP HANA Studio, SAP Business Application Studio, 또는 DBeaver
- 권한: 스키마에 대한 CREATE PROCEDURE 권한 필요
Procedure 기본 구문
SQLScript Procedure는 LANGUAGE SQLSCRIPT 키워드로 정의합니다. IN/OUT 파라미터를 통해 데이터를 주고받으며, READS SQL DATA를 붙이면 읽기 전용임을 옵티마이저에 알려주어 추가 최적화가 가능해집니다.
-- 기본 Procedure 구조
CREATE OR REPLACE PROCEDURE "MYSCHEMA"."PR_CALC_DISCOUNTS"
(
IN iv_discount_rate DECIMAL(5,2),
OUT et_result TABLE (
product_id NVARCHAR(10),
product_name NVARCHAR(100),
original_price DECIMAL(13,2),
discounted_price DECIMAL(13,2)
)
)
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER
READS SQL DATA
AS
BEGIN
-- 선언형(Declarative) 방식: SELECT 결과를 Table Variable에 할당
et_result = SELECT
p.product_id,
p.product_name,
p.price AS original_price,
p.price * (1 - :iv_discount_rate / 100) AS discounted_price
FROM "PRODUCTS" AS p
WHERE p.is_active = 1;
END;
핵심 키워드 설명 (접기/펼치기)
| 키워드 | 의미 |
|---|---|
LANGUAGE SQLSCRIPT | 프로시저 본문이 SQLScript 언어임을 선언 |
SQL SECURITY INVOKER | 호출자 권한으로 실행 (DEFINER는 생성자 권한) |
READS SQL DATA | 읽기 전용 표시. DML(INSERT/UPDATE/DELETE) 사용 불가. 옵티마이저가 병렬 실행 등 추가 최적화 적용 가능 |
Table Variable | 메모리 내 임시 결과 집합. 임시 테이블보다 오버헤드가 적음 |
READS SQL DATA를 지정하면 옵티마이저는 해당 Procedure가 데이터를 변경하지 않는다는 것을 보장받으므로, 실행 계획을 더 공격적으로 최적화할 수 있습니다. 읽기 전용 Procedure에는 반드시 이 옵션을 권장합니다. 자세한 구문은 SAP Help — Procedure Definition에서 확인할 수 있습니다.
3. 선언형 vs 명령형 — 어떤 패턴이 빠른가
SQLScript에는 두 가지 프로그래밍 스타일이 있습니다. 성능 차이를 이해하는 것이 최적화의 출발점입니다.
선언형(Declarative) 패턴
"무엇을 원하는지"를 기술합니다. SQL SELECT 문으로 Table Variable에 결과를 할당하는 방식입니다. HANA 옵티마이저가 실행 순서, 병렬화, 조인 전략을 자유롭게 결정할 수 있어 일반적으로 더 빠릅니다.
명령형(Imperative) 패턴
"어떻게 처리할지"를 기술합니다. IF/ELSE, WHILE, CURSOR, FOR 루프 등을 사용합니다. 옵티마이저의 최적화 여지가 줄어들고, 행 단위 처리가 되기 쉽습니다.
-- [나쁜 예] 명령형: 커서로 행 단위 처리
CREATE OR REPLACE PROCEDURE "PR_UPDATE_STATUS_BAD"()
LANGUAGE SQLSCRIPT AS
BEGIN
DECLARE CURSOR cur_orders FOR
SELECT order_id, total_amount FROM "ORDERS" WHERE status = 'PENDING';
DECLARE v_order_id NVARCHAR(10);
DECLARE v_amount DECIMAL(13,2);
OPEN cur_orders;
FETCH cur_orders INTO v_order_id, v_amount;
WHILE NOT cur_orders::NOTFOUND DO
IF :v_amount > 10000 THEN
UPDATE "ORDERS" SET status = 'VIP' WHERE order_id = :v_order_id;
ELSE
UPDATE "ORDERS" SET status = 'NORMAL' WHERE order_id = :v_order_id;
END IF;
FETCH cur_orders INTO v_order_id, v_amount;
END WHILE;
CLOSE cur_orders;
END;
-- [좋은 예] 선언형: 집합(Set-based) 연산
CREATE OR REPLACE PROCEDURE "PR_UPDATE_STATUS_GOOD"()
LANGUAGE SQLSCRIPT AS
BEGIN
UPDATE "ORDERS"
SET status = CASE
WHEN total_amount > 10000 THEN 'VIP'
ELSE 'NORMAL'
END
WHERE status = 'PENDING';
END;
성능 비교 팁
100만 건 기준으로 커서 방식은 수십 초가 걸릴 수 있지만, 집합 연산 방식은 1초 이내에 완료되는 경우가 일반적입니다. HANA의 컬럼 스토어는 집합 연산에 최적화되어 있으므로, 가능한 한 SELECT/UPDATE 단일 문장으로 처리하는 것을 권장합니다.
핵심 원칙은 간단합니다. 옵티마이저에게 자유를 주세요. 선언형으로 작성하면 HANA가 알아서 최적의 실행 경로를 찾습니다. 명령형은 비즈니스 로직이 복잡하여 SQL 단일 문장으로 표현이 불가능할 때만 사용하는 것을 권장합니다.
4. Table Variable Operator 활용법
Table Variable은 SQLScript의 핵심 개념입니다. 임시 테이블(Temporary Table)과 달리, Table Variable은 SQL 엔진을 거치지 않고 SQLScript 엔진 내부에서 직접 조작할 수 있습니다. 이를 가능하게 하는 것이 Table Variable Operator입니다.
비유하자면, 임시 테이블은 "창고에 물건을 넣었다 빼는 것"이고, Table Variable은 "작업대 위에 물건을 올려놓고 바로 가공하는 것"입니다. 중간에 창고를 거치지 않으므로 훨씬 빠릅니다.
CREATE OR REPLACE PROCEDURE "PR_SALES_ANALYSIS"
(
IN iv_year INTEGER,
OUT et_summary TABLE (
region NVARCHAR(50),
category NVARCHAR(50),
total_sales DECIMAL(15,2),
order_count INTEGER,
avg_order_value DECIMAL(13,2),
sales_rank INTEGER
)
)
LANGUAGE SQLSCRIPT
READS SQL DATA
AS
BEGIN
-- 1단계: 기본 데이터 추출 (Table Variable에 할당)
lt_base = SELECT
s.region,
p.category,
s.amount,
s.order_id
FROM "SALES" AS s
INNER JOIN "PRODUCTS" AS p ON s.product_id = p.product_id
WHERE YEAR(s.order_date) = :iv_year;
-- 2단계: Table Variable 기반 집계
lt_agg = SELECT
region,
category,
SUM(amount) AS total_sales,
COUNT(DISTINCT order_id) AS order_count,
SUM(amount) / COUNT(DISTINCT order_id) AS avg_order_value
FROM :lt_base
GROUP BY region, category;
-- 3단계: 윈도우 함수로 순위 부여
et_summary = SELECT
region,
category,
total_sales,
order_count,
avg_order_value,
ROW_NUMBER() OVER (
PARTITION BY region ORDER BY total_sales DESC
) AS sales_rank
FROM :lt_agg;
END;
위 예제에서 lt_base와 lt_agg는 Table Variable입니다. :lt_base처럼 콜론(:) 접두사를 붙여 참조합니다. 각 단계의 결과가 다음 단계의 입력이 되지만, HANA 옵티마이저는 이를 하나의 실행 계획으로 병합(Flattening)할 수 있어 성능이 우수합니다.
Table Variable vs 임시 테이블 비교
| 항목 | Table Variable | 임시 테이블 |
|---|---|---|
| 스코프 | Procedure 내부 | 세션 또는 글로벌 |
| DDL 필요 | 불필요 | CREATE 필요 |
| 옵티마이저 병합 | 가능 (Flattening) | 불가능 (경계 발생) |
| 병렬 실행 | 옵티마이저 판단 | 제한적 |
| 권장 용도 | 읽기 전용 중간 결과 | 세션 간 데이터 공유 |
더 자세한 Table Variable 문법과 연산자는 SAP HANA SQLScript Reference에서 확인할 수 있습니다.
5. Parallel Execution 활성화와 주의점
HANA는 기본적으로 Procedure 내 독립적인 SQL 문을 자동 병렬화하려고 시도합니다. 그러나 데이터 의존성이 감지되면 순차 실행으로 전환됩니다. 명시적으로 병렬 실행을 제어하는 방법이 있습니다.
PARALLEL EXECUTION 힌트
CREATE OR REPLACE PROCEDURE "PR_PARALLEL_REPORT"
(
OUT et_revenue TABLE (region NVARCHAR(50), revenue DECIMAL(15,2)),
OUT et_costs TABLE (department NVARCHAR(50), total_cost DECIMAL(15,2)),
OUT et_headcount TABLE (department NVARCHAR(50), emp_count INTEGER)
)
LANGUAGE SQLSCRIPT
READS SQL DATA
AS
BEGIN
-- 세 쿼리는 서로 독립적이므로 병렬 실행 가능
et_revenue = SELECT /*+ PARALLEL_EXECUTION */
region,
SUM(amount) AS revenue
FROM "SALES"
WHERE YEAR(order_date) = 2026
GROUP BY region;
et_costs = SELECT
department,
SUM(cost_amount) AS total_cost
FROM "COST_CENTER"
WHERE fiscal_year = 2026
GROUP BY department;
et_headcount = SELECT
department,
COUNT(*) AS emp_count
FROM "EMPLOYEES"
WHERE status = 'ACTIVE'
GROUP BY department;
END;
위 예제에서 세 SELECT 문은 서로 다른 테이블을 조회하며 의존성이 없습니다. HANA 옵티마이저는 이를 감지하여 병렬로 실행할 수 있습니다. /*+ PARALLEL_EXECUTION */ 힌트를 추가하면 옵티마이저에게 병렬화 의도를 더 명확히 전달합니다.
SEQUENTIAL EXECUTION이 필요한 경우
반대로, 특정 상황에서는 순차 실행이 필요합니다. Procedure 레벨에서 SEQUENTIAL EXECUTION 프래그마를 사용할 수 있습니다.
CREATE OR REPLACE PROCEDURE "PR_SEQUENTIAL_ETL"()
LANGUAGE SQLSCRIPT
AS
BEGIN
-- DML 순서가 중요한 ETL 시나리오
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
-- 1단계: 기존 데이터 정리
DELETE FROM "STAGING_TABLE" WHERE load_date < ADD_DAYS(CURRENT_DATE, -30);
-- 2단계: 신규 데이터 적재 (1단계 완료 후 실행되어야 함)
INSERT INTO "STAGING_TABLE"
SELECT *, CURRENT_DATE AS load_date
FROM "SOURCE_TABLE"
WHERE modified_date >= ADD_DAYS(CURRENT_DATE, -1);
-- 3단계: 집계 테이블 갱신 (2단계 완료 후 실행되어야 함)
MERGE INTO "SUMMARY_TABLE" AS t
USING (
SELECT category, SUM(amount) AS total
FROM "STAGING_TABLE"
GROUP BY category
) AS s
ON t.category = s.category
WHEN MATCHED THEN UPDATE SET t.total = s.total
WHEN NOT MATCHED THEN INSERT VALUES (s.category, s.total);
END;
주의: 병렬 실행이 항상 빠르지는 않습니다
병렬 실행은 각 쿼리가 충분히 무거울 때 효과적입니다. 가벼운 쿼리 여러 개를 병렬화하면 스레드 관리 오버헤드가 오히려 성능을 저하시킬 수 있습니다. 또한 동시 실행 쿼리가 동일 테이블을 대량 스캔하면 메모리 경합이 발생할 수 있습니다. PlanViz로 실제 실행 계획을 확인하여 병렬화가 효과적인지 검증하는 것을 권장합니다.
Procedure 실행 모드에 관한 상세 내용은 SAP Help — Procedure Definition 문서를 참고하세요.
6. CE Function vs SQL — 현재 권장 사항
과거 SAP HANA 초기 버전에서는 CE Function(Calculation Engine Function)이 성능상 유리하다고 알려졌습니다. CE_CALC, CE_PROJECTION, CE_JOIN, CE_AGGREGATION 등의 함수가 SQL 파서를 우회하여 HANA 계산 엔진을 직접 호출했기 때문입니다.
하지만 현재 SAP HANA 2.0 및 HANA Cloud에서는 표준 SQL 사용을 권장합니다. 그 이유는 다음과 같습니다.
- 옵티마이저 개선: 최신 HANA 옵티마이저는 표준 SQL도 CE Function과 동등하게 최적화합니다.
- 가독성: SQL은 개발자 누구나 읽을 수 있지만, CE Function은 HANA 전용 문법이어서 유지보수가 어렵습니다.
- 기능 제한: CE Function은 모든 SQL 기능을 지원하지 않으며, 윈도우 함수 등 고급 기능을 사용할 수 없습니다.
- 향후 지원: SAP는 CE Function보다 SQL 최적화에 투자하고 있어, 장기적으로 SQL이 더 안정적입니다.
결론: 신규 개발에서는 표준 SQL을 사용하세요. 기존 CE Function 코드는 동작하지만, 리팩토링 기회가 있을 때 SQL로 전환하는 것을 권장합니다.
7. AMDP로 ABAP에서 SQLScript 활용하기
AMDP(ABAP Managed Database Procedure)는 ABAP 클래스 메서드 안에 SQLScript를 직접 작성할 수 있게 해주는 메커니즘입니다. S/4HANA 환경에서 Code Pushdown을 구현하는 핵심 기술입니다.
CLASS zcl_sales_amdp DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_amdp_marker_hdb.
" 출력용 구조체 타입
TYPES: BEGIN OF ty_sales_summary,
region TYPE char50,
total_sales TYPE p LENGTH 15 DECIMALS 2,
order_count TYPE i,
END OF ty_sales_summary,
tt_sales_summary TYPE STANDARD TABLE OF ty_sales_summary WITH EMPTY KEY.
" AMDP 메서드 선언
CLASS-METHODS get_sales_summary
IMPORTING VALUE(iv_year) TYPE i
EXPORTING VALUE(et_result) TYPE tt_sales_summary
RAISING cx_amdp_execution_failed.
ENDCLASS.
CLASS zcl_sales_amdp IMPLEMENTATION.
METHOD get_sales_summary
BY DATABASE PROCEDURE FOR HDB
LANGUAGE SQLSCRIPT
OPTIONS READ-ONLY
USING zsales_table.
" SQLScript 본문 — HANA에서 직접 실행됨
et_result = SELECT
region,
SUM(amount) AS total_sales,
COUNT(*) AS order_count
FROM "ZSALES_TABLE"
WHERE fiscal_year = :iv_year
GROUP BY region
ORDER BY total_sales DESC;
ENDMETHOD.
ENDCLASS.
AMDP 작성 시 주의사항
if_amdp_marker_hdb인터페이스를 반드시 구현해야 합니다.BY DATABASE PROCEDURE FOR HDB로 대상 DB를 명시합니다.OPTIONS READ-ONLY는 SQLScript의READS SQL DATA에 대응합니다.USING절에 접근할 테이블/뷰를 명시해야 합니다. 누락하면 컴파일 에러가 발생합니다.- AMDP 내부에서는 ABAP 변수를 직접 참조할 수 없고, IMPORTING 파라미터만 사용 가능합니다.
AMDP는 기존 ABAP SELECT 문으로는 표현하기 어려운 복잡한 집계, 윈도우 함수, 다단계 변환 로직을 HANA DB에서 직접 실행할 수 있게 해줍니다. 다만 단순한 SELECT는 Open SQL로 충분하므로, AMDP는 성능상 이점이 명확할 때 사용하는 것을 권장합니다.
8. PlanViz & Expensive Statement Trace로 병목 잡기
최적화의 전제 조건은 측정입니다. "느리다"는 감이 아닌 데이터로 확인해야 합니다. SAP HANA는 두 가지 핵심 분석 도구를 제공합니다.
PlanViz (Plan Visualizer)
PlanViz는 SQL 실행 계획을 시각적으로 보여주는 도구입니다. SAP HANA Studio에서 SQL 문을 선택하고 "Visualize Plan"을 실행하면, 각 연산자(조인, 스캔, 집계 등)의 실행 시간, 처리 행 수, 메모리 사용량을 트리 형태로 확인할 수 있습니다.
- Estimated Plan: 실제 실행 없이 옵티마이저의 예상 계획을 확인합니다.
- Executed Plan: 실제 실행 후 각 단계의 실측 데이터를 확인합니다. 성능 분석에는 이 모드를 권장합니다.
PlanViz에서 확인할 핵심 지표는 다음과 같습니다.
| 지표 | 확인 포인트 |
|---|---|
| Exclusive Time | 해당 연산자 자체 실행 시간. 이 값이 큰 노드가 병목 |
| Output Records | 각 단계의 출력 행 수. 급격히 늘어나는 지점이 있으면 조인 조건 확인 |
| Operator | Column Search vs Row Search. Row Search가 나타나면 인덱스 부재 의심 |
Expensive Statement Trace
특정 시간 동안 실행된 SQL 중 임계값(실행 시간, 메모리 사용량 등)을 초과한 문장을 기록합니다. 운영 환경에서 간헐적으로 발생하는 성능 이슈를 추적할 때 유용합니다.
활성화 방법: SAP HANA Studio > Administration > Trace Configuration > Expensive Statement Trace에서 임계값을 설정합니다. 일반적으로 실행 시간 1초 이상을 기준으로 설정하는 경우가 많습니다.
Expensive Statement Trace 활성화 시 주의
트레이스가 활성화되면 시스템에 약간의 오버헤드가 발생합니다. 운영 환경에서는 문제 분석 시에만 한시적으로 활성화하고, 분석 완료 후 비활성화하는 것을 권장합니다. 또한 트레이스 결과에 민감한 SQL 데이터가 포함될 수 있으므로 접근 권한 관리에 주의하세요.
성능 분석 도구의 상세 활용법은 SAP HANA Performance Developer Guide에서 확인할 수 있습니다.
흔한 실수 / 트러블슈팅
FAQ
Q1. READS SQL DATA를 지정했는데 INSERT 문을 쓰면 어떻게 되나요?
컴파일 시점에 오류가 발생합니다. READS SQL DATA는 DML을 금지하는 제약이므로, 데이터를 변경해야 하는 Procedure에서는 이 옵션을 제거해야 합니다. 읽기 전용 Procedure와 변경 Procedure를 분리 설계하는 것이 좋은 패턴입니다.
Q2. Table Variable이 너무 크면 메모리 문제가 생기지 않나요?
Table Variable도 메모리를 사용합니다. 수억 건 이상의 데이터를 한 번에 Table Variable에 담는 것은 피해야 합니다. 대용량 처리 시에는 WHERE 조건으로 범위를 제한하거나, 집계를 먼저 수행하여 데이터량을 줄인 후 다음 단계로 전달하세요.
Q3. 동적 SQL을 꼭 써야 하는 상황은 없나요?
테이블명이나 컬럼명이 런타임에 결정되는 경우에는 동적 SQL이 불가피합니다. 다만 이 경우에도 EXEC 대신 APPLY_FILTER 함수를 고려해 보세요. APPLY_FILTER는 WHERE 조건만 동적으로 지정할 수 있어, 옵티마이저가 나머지 부분을 최적화할 수 있습니다.
Q4. CE Function이 포함된 기존 코드를 당장 바꿔야 하나요?
당장 동작에 문제가 생기지는 않습니다. 다만 신규 개발에서는 SQL을 사용하고, 기존 코드는 리팩토링 일정에 맞춰 점진적으로 전환하는 전략이 현실적입니다.
다음 단계 / 관련 주제
이 튜토리얼에서 다룬 내용을 기반으로, 다음 주제를 탐색해 보세요.
- CDS View와 AMDP의 조합 — CDS View에서 표현하기 어려운 로직을 AMDP Table Function으로 보완하는 패턴
- SQLScript 디버거 활용 — SAP HANA Studio에서 Procedure를 단계별로 디버깅하는 방법
- HANA Predictive Analysis Library (PAL) — SQLScript 내에서 머신러닝 알고리즘을 호출하는 고급 활용
- Workload Management — 다수 사용자가 동시에 무거운 Procedure를 호출할 때 리소스 배분 전략
- SQLScript Code Analyzer — 코드 품질과 성능 안티패턴을 자동으로 탐지하는 도구