BTP

HANA Inner vs Left vs Cross JOIN 비교 #shorts #SAP #HANA

인메모리 DB에서 JOIN이 성능을 좌우하는 이유

SAP HANA(HANA Cloud 및 HANA 2.0 SPS07 기준)는 데이터를 디스크가 아닌 메모리에 컬럼 단위로 저장합니다. 디스크 I/O가 병목이던 전통적인 DB와 달리, HANA에서는 JOIN 시 얼마나 많은 중간 결과(Intermediate Result)를 만들어내느냐가 성능의 핵심 변수가 됩니다. 컬럼 스토어는 딕셔너리 인코딩으로 압축된 정수 값끼리 매칭하기 때문에 JOIN 자체는 빠르지만, JOIN 유형을 잘못 고르면 필요 없는 행이 수백만 건씩 메모리에 펼쳐지면서 CPU와 메모리를 동시에 소모합니다.

이 글에서는 가장 자주 쓰이는 세 가지 JOIN — Inner, Left Outer, Cross — 의 동작 원리를 비유와 실행 계획으로 비교하고, 대용량 데이터에서 어떤 선택이 유리한지 실전 예제로 확인합니다. 읽고 나면 다음을 스스로 점검할 수 있습니다.

  • 세 JOIN 유형의 결과 행 수 차이를 예측할 수 있다
  • EXPLAIN PLAN으로 JOIN 실행 계획을 읽을 수 있다
  • Outer Join에서 ON 조건과 WHERE 조건의 차이를 설명할 수 있다
  • 파티션 프루닝으로 JOIN 대상 데이터를 줄일 수 있다

미리 알아두면 좋은 것은 기본 SELECT/WHERE 문법과 기본 키·외래 키 개념 정도입니다. SQL 초심자도 따라올 수 있도록 단계별로 설명합니다. 실습 환경으로는 SAP HANA Cloud 무료 체험(trial) 인스턴스 또는 SAP HANA, express edition을 권장하며, SQL 콘솔은 SAP HANA Database Explorer를 사용합니다.

Inner Join — 교집합만 남기는 가장 예측 가능한 선택

Inner Join은 두 테이블에서 조인 조건을 만족하는 행만 반환합니다. 비유하자면 "회원 명부"와 "이번 달 주문서"를 겹쳐 놓고, 양쪽에 모두 이름이 있는 사람만 뽑는 것입니다. 매칭되지 않는 행은 결과에서 사라지므로, 일반적으로 결과 집합이 가장 작고 옵티마이저가 최적화하기도 가장 쉽습니다.

-- 1단계: 기본 예제 — 최근 주문과 고객명 조회
SELECT so.SALES_ORDER_ID,
       so.ORDER_DATE,
       so.NET_AMOUNT,
       cm.CUSTOMER_NAME
  FROM SALES_ORDER AS so
 INNER JOIN CUSTOMER_MASTER AS cm
    ON so.CUSTOMER_ID = cm.CUSTOMER_ID
 WHERE so.ORDER_DATE >= '2026-01-01';

HANA 컬럼 엔진은 이때 CUSTOMER_ID 컬럼의 딕셔너리 값(정수 ID)끼리 해시 매칭을 수행합니다. 문자열 전체를 비교하지 않기 때문에 조인 키의 데이터 타입이 양쪽에서 동일하면 매우 빠릅니다. 반대로 한쪽이 NVARCHAR, 다른 쪽이 INTEGER면 암묵적 형 변환이 발생해 딕셔너리 매칭 이점이 사라지므로 주의해야 합니다.

Left Outer Join — 기준 테이블을 지키는 JOIN과 NULL 처리

Left Outer Join은 왼쪽(기준) 테이블의 모든 행을 보존하고, 오른쪽에서 매칭되는 행이 없으면 해당 컬럼을 NULL로 채웁니다. "전체 고객 명부를 기준으로, 주문이 있으면 붙이고 없으면 빈칸"인 셈입니다. 주문이 없는 고객까지 리포트에 포함해야 하는 실무 요구사항에서 필수적입니다.

-- 2단계: 실무 시나리오 — 최근 3개월 주문이 없는 고객 포함 집계
SELECT cm.CUSTOMER_ID,
       cm.CUSTOMER_NAME,
       COALESCE(SUM(so.NET_AMOUNT), 0) AS TOTAL_AMOUNT,
       COUNT(so.SALES_ORDER_ID)        AS ORDER_COUNT
  FROM CUSTOMER_MASTER AS cm
  LEFT OUTER JOIN SALES_ORDER AS so
    ON cm.CUSTOMER_ID = so.CUSTOMER_ID
   AND so.ORDER_DATE >= ADD_MONTHS(CURRENT_DATE, -3)
 GROUP BY cm.CUSTOMER_ID, cm.CUSTOMER_NAME
 ORDER BY TOTAL_AMOUNT DESC;

여기서 가장 흔한 함정이 하나 있습니다. 날짜 필터를 WHERE so.ORDER_DATE >= ...로 옮기면, 주문이 없는 고객의 ORDER_DATE는 NULL이라 조건에서 탈락하고 결과가 사실상 Inner Join과 같아집니다. Outer Join에서 오른쪽 테이블에 대한 필터는 ON 절에 두는 것이 일반적으로 올바른 패턴입니다. NULL 값은 COALESCE로 기본값을 지정해 후속 계산 오류를 막습니다.

성능 측면에서 Left Outer Join은 기준 테이블 행 수만큼의 결과가 보장되어야 하므로, 옵티마이저가 조인 순서를 바꾸거나 매칭 안 되는 행을 조기에 버리는 최적화를 적용하기 어렵습니다. 같은 데이터라면 Inner Join보다 느리거나 같은 수준이라고 보는 것이 안전합니다.

Cross Join — 카테시안 곱, 위험하지만 쓸 곳이 있다

Cross Join은 조인 조건 없이 두 테이블의 모든 조합(카테시안 곱)을 만듭니다. 왼쪽 1만 행 × 오른쪽 1만 행이면 결과는 1억 행입니다. 인메모리 DB에서 이 중간 결과는 그대로 메모리를 점유하므로, 대용량 테이블 간 Cross Join은 OOM(메모리 부족)으로 이어질 수 있습니다.

그렇다고 무조건 금지 대상은 아닙니다. 작은 기준 테이블끼리 조합 매트릭스를 만들 때는 오히려 정석입니다.

-- 플랜트 × 통화 조합표 생성 (양쪽 모두 소규모 마스터)
SELECT pl.PLANT_ID, cur.CURRENCY_CODE
  FROM PLANT_LIST AS pl
 CROSS JOIN CURRENCY_LIST AS cur;

주의할 점은 의도치 않은 Cross Join입니다. Inner Join을 쓰려다 ON 조건을 빠뜨리거나, 콤마 조인(FROM A, B)에서 WHERE 조건을 누락하면 카테시안 곱이 발생합니다. 쿼리가 갑자기 수십 배 느려졌다면 실행 계획에서 예상 행 수가 폭증했는지부터 확인하는 것이 좋습니다.

실행 계획으로 보는 성능 차이 — EXPLAIN PLAN 분석

세 JOIN의 차이는 실행 계획에서 OUTPUT_SIZE(예상 행 수)로 명확히 드러납니다.

-- 3단계: 프로덕션 — 실행 계획 확인 절차
EXPLAIN PLAN SET STATEMENT_NAME = 'JOIN_TEST' FOR
SELECT so.SALES_ORDER_ID, cm.CUSTOMER_NAME
  FROM SALES_ORDER AS so
 INNER JOIN CUSTOMER_MASTER AS cm
    ON so.CUSTOMER_ID = cm.CUSTOMER_ID;

SELECT OPERATOR_NAME, TABLE_NAME, OUTPUT_SIZE, EXECUTION_ENGINE
  FROM SYS.EXPLAIN_PLAN_TABLE
 WHERE STATEMENT_NAME = 'JOIN_TEST'
 ORDER BY OPERATOR_ID;

고객 10만 건, 주문 1,000만 건(고객당 평균 100건, 미주문 고객 5%)을 가정한 대략적 비교입니다.

JOIN 유형결과 행 수(예)중간 결과 부담주 용도
Inner약 1,000만낮음 — 매칭 행만 생성양쪽 모두 존재하는 데이터 조회
Left Outer1,000만 + 미매칭 고객 5천중간 — 기준 행 보존 필요결측 포함 리포트, 마스터 기준 집계
Cross10만 × 1,000만 = 1조극단적 — 사실상 실행 불가소규모 조합표 생성에 한정

더 깊은 분석이 필요하면 SAP HANA Cockpit 또는 Database Explorer의 PlanViz(SQL Analyzer)로 각 연산자의 실제 처리 행 수와 소요 시간을 시각적으로 확인하는 것을 권장합니다. JOIN 노드의 입력 행 수 대비 출력 행 수 비율이 비정상적으로 크면 조인 조건 누락이나 키 중복을 의심할 수 있습니다.

대용량 데이터에서의 주의사항 — 파티션 프루닝과 조인 키 설계

JOIN 성능 최적화의 절반은 "조인에 들어가는 행 자체를 줄이는 것"입니다. HANA에서 대표적인 수단이 파티션 프루닝입니다. 날짜 범위로 파티션된 테이블에 날짜 필터를 걸면, 조건에 맞는 파티션만 스캔하고 나머지는 건너뜁니다.

-- 주문 테이블을 연도 범위로 파티셔닝
ALTER TABLE SALES_ORDER
  PARTITION BY RANGE (ORDER_DATE)
  ( PARTITION '2024-01-01' <= VALUES < '2025-01-01',
    PARTITION '2025-01-01' <= VALUES < '2026-01-01',
    PARTITION OTHERS );
  • 조인 키 타입 통일: 양쪽 컬럼의 타입·길이를 맞춰 암묵적 변환을 제거
  • 필터 먼저, 조인 나중: WHERE 조건이 조인 전에 적용되도록 서브쿼리·CTE로 명시하는 것도 유효
  • 키 중복 확인: 오른쪽 테이블 키가 유일하지 않으면 행이 곱으로 불어남(팬아웃)
  • 일반적으로 컬럼 스토어에서는 별도 인덱스 없이도 조인이 빠르지만, OLTP성 고선택도 조회가 반복되면 조인 키에 인덱스 생성 검토

실전 시나리오 — SalesOrder × CustomerMaster 리포트 최적화

"전 고객 대상 분기 매출 리포트가 40초 걸린다"는 상황을 가정합니다. 원인 분석과 개선을 단계별로 진행합니다.

  1. 실행 계획 확인: EXPLAIN PLAN에서 JOIN의 OUTPUT_SIZE가 전체 주문 1,000만 건 그대로 — 날짜 필터가 조인 뒤에 적용되고 있음
  2. 필터 선적용: 분기 조건을 CTE로 먼저 적용해 조인 대상 축소
  3. 파티션 프루닝 활용: ORDER_DATE 파티션 덕분에 해당 분기 파티션만 스캔
WITH RECENT_ORDERS AS (
    SELECT CUSTOMER_ID, NET_AMOUNT
      FROM SALES_ORDER
     WHERE ORDER_DATE BETWEEN '2026-04-01' AND '2026-06-30'
)
SELECT cm.CUSTOMER_NAME,
       COALESCE(SUM(ro.NET_AMOUNT), 0) AS Q2_AMOUNT
  FROM CUSTOMER_MASTER AS cm
  LEFT OUTER JOIN RECENT_ORDERS AS ro
    ON cm.CUSTOMER_ID = ro.CUSTOMER_ID
 GROUP BY cm.CUSTOMER_NAME;

이 패턴만으로 조인 입력이 1,000만 → 약 250만 건으로 줄어, 일반적으로 수 배의 개선을 기대할 수 있습니다. "주문 없는 고객 포함" 요구가 없다면 Inner Join으로 바꿔 추가 이득을 얻습니다.

  • Q1. Left Join이 Inner Join과 같은 결과를 내는데 왜 느린가요? — WHERE 절이 오른쪽 테이블을 필터링해 사실상 Inner로 동작 중일 가능성이 큽니다. 의미가 Inner라면 Inner로 명시해야 옵티마이저가 더 자유롭게 최적화합니다.
  • Q2. 조인 후 행 수가 원본보다 많아졌어요. — 오른쪽 테이블 조인 키에 중복이 있는 팬아웃 현상입니다. GROUP BY로 유일성을 확보하거나 키 설계를 재검토하세요.
  • Q3. 쿼리가 메모리 한도 오류로 죽습니다. — 실행 계획에서 카테시안 곱(조인 조건 누락) 여부를 먼저 확인하고, 워크로드 클래스로 문장별 메모리 한도를 설정하는 것도 방법입니다.

정리 — 어떤 JOIN을 언제 쓸 것인가

핵심 선택 기준은 간단합니다. 양쪽 모두 있어야 의미 있는 데이터면 Inner, 기준 테이블 전체를 보존해야 하면 Left Outer(필터는 ON 절에), 소규모 조합표가 필요할 때만 Cross. 그리고 어떤 경우든 조인 전에 데이터를 줄이는 것(필터 선적용, 파티션 프루닝)이 조인 자체 튜닝보다 효과가 큰 경우가 많습니다.

댓글 0

아직 댓글이 없습니다.