BTP

아직도 전부 컬럼 테이블? — 세션·큐는 로우가 답 #shorts #SAP #HANA

▶ YouTube에서 보기

이 글이 답하는 질문

SAP HANA Cloud에서 CREATE TABLE을 실행하면 기본적으로 컬럼 테이블이 만들어집니다. 그래서 많은 개발자가 "HANA는 무조건 컬럼"이라고 외우고 넘어갑니다. 대부분의 경우 그 선택이 맞습니다. 하지만 모든 테이블을 컬럼으로 만들면 오히려 손해를 보는 워크로드가 분명히 존재합니다. 이 글에서는 컬럼 스토어와 로우 스토어가 내부적으로 어떻게 다른지, 어떤 쿼리 패턴에서 성능이 갈리는지, 그리고 로우 테이블이 더 나은 선택이 되는 구체적인 3가지 경우를 실습 코드와 함께 정리합니다.

  • 컬럼 스토어와 로우 스토어의 저장 구조 차이와 그것이 쿼리 성능에 미치는 영향
  • 집계·분석 쿼리와 단건 갱신 루프에서 두 스토어의 성능이 뒤집히는 이유
  • 로우 테이블이 맞는 3가지 실무 케이스 (큐·세션 테이블, 좁은 매핑 테이블, 쓰기 폭주 임시 테이블)
  • HANA Cloud 환경에서 로우 테이블을 쓸 때 반드시 알아야 할 제약

사전 가정

SQL 기본 문법(DDL, DML, GROUP BY)을 알고 있고, SAP HANA Cloud 인스턴스(트라이얼 또는 프로덕션)에 SAP HANA Database Explorer나 hdbsql로 접속할 수 있다고 가정합니다. 예제는 SAP HANA Cloud, SAP HANA Database(2026년 기준 QRC 릴리스) 기준이며, 온프레미스 HANA 2.0에서도 개념은 동일하지만 일부 제약 사항이 다를 수 있습니다.

컬럼 스토어 vs 로우 스토어 — 구조가 성능을 결정한다

두 스토어의 차이를 엑셀 시트에 비유하면 이해가 빠릅니다. 로우 스토어는 시트를 행 단위로 저장합니다. 주문 1건의 모든 정보(주문번호, 고객, 금액, 상태)가 메모리에서 붙어 있으므로, "주문 1건 전체를 읽거나 고치는" 작업이 매우 쌉니다. 반면 컬럼 스토어는 시트를 열 단위로 잘라 저장합니다. 금액 컬럼만 쭉 이어져 있으므로 "전체 주문의 금액 합계" 같은 작업은 필요한 열만 스캔하면 끝납니다.

컬럼 스토어가 HANA의 기본이 된 이유는 여기에 두 가지 무기가 더해지기 때문입니다.

  • 사전(dictionary) 압축 — 같은 컬럼에는 비슷한 값이 반복되므로, 값 사전과 정수 인덱스로 치환해 메모리 사용량을 크게 줄입니다. 상태 코드처럼 카디널리티가 낮은 컬럼은 압축률이 극단적으로 높아집니다.
  • 델타 머지(delta merge) 구조 — 압축된 메인 저장소는 수정이 비싸므로, 신규 쓰기는 비압축 델타 영역에 먼저 쌓고 주기적으로 메인에 병합합니다. 읽기 최적화와 쓰기 수용을 동시에 잡는 구조입니다.

바로 이 두 무기가 약점이 되는 순간이 있습니다. 단건 행 전체를 읽으려면 컬럼 스토어는 여러 컬럼 조각을 다시 조립해야 하고, 쓰기가 폭주하면 델타 영역이 비대해져 머지 비용이 시스템을 압박합니다. 로우 스토어는 압축도 델타 머지도 없는 대신, 행 단위 접근이 구조적으로 가장 빠릅니다. 즉 "어느 쪽이 빠른가"는 테이블이 아니라 워크로드가 결정합니다.

로우 테이블이 맞는 3가지 경우

일반적으로 다음 3가지 패턴에 해당하면 로우 테이블을 검토할 가치가 있습니다.

  1. 초고빈도 단건 UPDATE/INSERT가 몰리는 작은 테이블 — 메시지 큐, 로그인 세션, 시퀀스 대용 카운터처럼 초당 수백~수천 건의 단건 갱신이 반복되는 테이블입니다. 컬럼 테이블에서는 갱신마다 델타 영역에 새 버전이 쌓이고 머지 부담이 누적되지만, 로우 테이블은 해당 행만 제자리에서 수정합니다.
  2. 항상 전체 행을 point read 하는 좁은 설정·매핑 테이블 — 코드-명칭 매핑, 기능 플래그, 인터페이스 라우팅 설정처럼 컬럼 수가 적고 행 수도 수천 건 이하이며, 조회가 항상 "키로 1행 전체"인 테이블입니다. 집계할 일도, 압축으로 아낄 메모리도 없으므로 컬럼 스토어의 장점이 발휘될 여지가 없습니다.
  3. 델타 머지 오버헤드가 문제 되는 쓰기 폭주 임시 테이블 — 배치 인터페이스의 스테이징처럼 대량으로 넣고, 잠깐 쓰고, 통째로 비우는 테이블입니다. 데이터 수명이 머지 주기보다 짧다면 압축 비용만 내고 이득은 못 챙기는 셈이 됩니다. (다만 이 경우 글로벌 임시 테이블이나 배치 후 수동 머지 등 대안도 함께 검토하는 것이 좋습니다.)

공통점을 뽑으면 작고, 행 단위로 접근하며, 분석하지 않는 테이블입니다. 반대로 행 수가 수십만을 넘거나 GROUP BY·집계·범위 스캔 대상이 되는 순간, 답은 거의 항상 컬럼입니다.

직접 해보기 — 같은 데이터, 다른 스토어, 갈리는 성능

주문 이력(분석용)과 로그인 세션(갱신용) 시나리오로 두 스토어를 직접 비교해 봅니다. 먼저 동일한 구조의 테이블을 스토어만 다르게 만듭니다. HANA Cloud에서 스토어 지정 키워드를 생략하면 컬럼 테이블이 생성됩니다.

-- 1) 분석 워크로드용 주문 이력: 컬럼(기본) vs 로우
CREATE COLUMN TABLE zord_hist_col (
  ord_id     BIGINT       PRIMARY KEY,
  cust_no    INTEGER      NOT NULL,
  ord_date   DATE         NOT NULL,
  status_cd  NVARCHAR(2)  NOT NULL,
  net_amt    DECIMAL(15,2)
);

CREATE ROW TABLE zord_hist_row (
  ord_id     BIGINT       PRIMARY KEY,
  cust_no    INTEGER      NOT NULL,
  ord_date   DATE         NOT NULL,
  status_cd  NVARCHAR(2)  NOT NULL,
  net_amt    DECIMAL(15,2)
);

-- 2) 테스트 데이터 500만 건 적재 (SERIES_GENERATE 활용)
INSERT INTO zord_hist_col
SELECT element_number,
       MOD(element_number, 40000) + 1,
       ADD_DAYS('2024-01-01', MOD(element_number, 730)),
       CASE MOD(element_number, 4) WHEN 0 THEN 'CP'
            WHEN 1 THEN 'OP' WHEN 2 THEN 'SH' ELSE 'CX' END,
       MOD(element_number, 9000) / 100 + 10
FROM SERIES_GENERATE_INTEGER(1, 1, 5000001);

INSERT INTO zord_hist_row SELECT * FROM zord_hist_col;
MERGE DELTA OF zord_hist_col;  -- 적재 후 수동 머지로 조건 동일화

이제 분석 쿼리를 양쪽에 던져 봅니다. 실행 전후로 실행 시간을 확인하거나 SQL 콘솔의 소요 시간 표시를 비교하면 됩니다.

-- 고객·상태별 매출 집계 (전형적 분석 쿼리)
SELECT status_cd, COUNT(*), SUM(net_amt), AVG(net_amt)
FROM zord_hist_col
WHERE ord_date >= '2025-01-01'
GROUP BY status_cd;

SELECT status_cd, COUNT(*), SUM(net_amt), AVG(net_amt)
FROM zord_hist_row
WHERE ord_date >= '2025-01-01'
GROUP BY status_cd;

제 테스트 환경(HANA Cloud 트라이얼, 소형 인스턴스)에서는 컬럼 테이블이 수십 밀리초, 로우 테이블이 수 초 단위로 나와 수십 배 차이가 났습니다. 컬럼 쪽은 status_cd, ord_date, net_amt 세 컬럼만 압축 상태로 스캔하지만, 로우 쪽은 500만 행 전체를 행 단위로 훑어야 하기 때문입니다. 절대 수치는 인스턴스 크기와 데이터 분포에 따라 크게 달라지므로 반드시 본인 환경에서 재현해 보길 권합니다.

이번엔 반대 상황입니다. 세션 테이블에 단건 갱신을 반복하는 OLTP성 루프를 프로시저로 흉내 냅니다.

-- 3) 갱신 워크로드용 세션 테이블
CREATE ROW TABLE zauth_sess_row (
  sess_id    NVARCHAR(32) PRIMARY KEY,
  user_no    INTEGER,
  hit_cnt    INTEGER,
  last_seen  TIMESTAMP
);
CREATE COLUMN TABLE zauth_sess_col LIKE zauth_sess_row;

CREATE PROCEDURE zp_sess_burst(IN iv_target NVARCHAR(10))
AS BEGIN
  DECLARE i INTEGER;
  FOR i IN 1..20000 DO
    IF :iv_target = 'ROW' THEN
      UPSERT zauth_sess_row
        VALUES ('S' || MOD(:i, 500), MOD(:i, 500), :i, CURRENT_TIMESTAMP)
        WITH PRIMARY KEY;
    ELSE
      UPSERT zauth_sess_col
        VALUES ('S' || MOD(:i, 500), MOD(:i, 500), :i, CURRENT_TIMESTAMP)
        WITH PRIMARY KEY;
    END IF;
  END FOR;
END;

CALL zp_sess_burst('ROW');  -- vs CALL zp_sess_burst('COL');

같은 환경에서 이 루프는 로우 테이블이 컬럼 테이블보다 눈에 띄게 빨랐고, 컬럼 쪽은 실행 후 델타 영역이 부풀어 있는 것을 확인할 수 있었습니다. 델타 상태는 모니터링 뷰로 직접 볼 수 있습니다.

-- 컬럼 테이블의 델타 영역 크기 확인
SELECT table_name, raw_record_count_in_delta,
       memory_size_in_delta, memory_size_in_main
FROM m_cs_tables
WHERE table_name LIKE 'ZAUTH_SESS%' OR table_name LIKE 'ZORD_HIST%';

-- 로우 테이블 메모리 사용량 확인
SELECT table_name, allocated_fixed_part_size, used_fixed_part_size
FROM m_rs_tables
WHERE table_name LIKE 'Z%';

-- 이미 만든 테이블의 스토어 전환도 가능 (락·재구성 비용 주의)
ALTER TABLE zauth_sess_col ALTER TYPE ROW;

정리하면, 실측에서도 구조가 예고한 그대로 결과가 나옵니다. 넓게 읽고 집계하면 컬럼, 좁게 반복 갱신하면 로우입니다.

삽질 노트

Q1. 로우 테이블로 바꿨더니 메모리가 더 부담스러워졌어요. 왜죠?

로우 스토어는 압축이 없고, 일반적으로 전체가 메모리에 상주하는 것을 전제로 동작합니다. 컬럼 테이블처럼 컬럼 단위 로드/언로드나 NSE(Native Storage Extension) 기반의 디스크 오프로딩을 활용할 수 없습니다. 그래서 "큰 테이블을 로우로" 전환하는 것은 거의 항상 악수입니다. 로우 스토어는 작게 유지하는 것이 원칙이며, HANA Cloud에서는 로우 스토어 전체 크기가 인스턴스 메모리를 압박하지 않는지 M_RS_TABLES로 주기적으로 확인하는 것이 좋습니다.

Q2. 세션 테이블을 로우로 바꿨는데 기대만큼 빨라지지 않았습니다.

병목이 스토어가 아닐 가능성이 큽니다. 커밋 빈도(건별 커밋 vs 배치 커밋), 락 경합, 애플리케이션-DB 왕복 횟수가 먼저 성능을 잡아먹는 경우가 많습니다. 스토어 전환 전에 EXPLAIN PLAN과 expensive statement 추적으로 실제 병목을 확인하고, 컬럼 테이블 유지 + 배치 커밋만으로 충분한지 먼저 검증하길 권합니다.

Q3. 로우 테이블에서는 파티셔닝이나 분석 기능을 못 쓰나요?

로우 테이블은 파티셔닝 대상이 아니고, 컬럼 스토어를 전제로 하는 기능(일부 고급 압축, 텍스트/공간 처리의 일부 기능 등)에서 제외됩니다. CDS 뷰나 계산 뷰의 분석 대상이 될 테이블이라면 처음부터 컬럼으로 두는 것이 맞습니다. 또한 백업·복제 관점에서도 로우 스토어 영역은 별도로 관리되므로, 전환 전에 운영팀과 영향 범위를 공유하는 것이 안전합니다.

Q4. 임시 스테이징을 로우로 만들었는데 더 좋은 방법이 있다고요?

세션 수명만큼만 데이터가 사는 경우라면 글로벌 임시 테이블(CREATE GLOBAL TEMPORARY TABLE)이 로그·머지 부담 면에서 더 깔끔할 수 있습니다. 영속 스테이징이 필요하면 컬럼 테이블 + 배치 적재 후 MERGE DELTA 수동 실행(자동 머지 비활성 조합)도 흔히 쓰는 패턴입니다. 로우 테이블은 그 중간 지대의 선택지 중 하나일 뿐입니다.

핵심 한 줄

기본은 컬럼, 예외만 로우 — 그리고 그 예외 3가지(고빈도 단건 갱신 소형 테이블, 전체 행 point read 매핑 테이블, 머지 비용이 아까운 쓰기 폭주 임시 테이블)를 아는 것이 곧 선택 기준이다.

더 파볼 주제

이 글의 실습을 마쳤다면 델타 머지 파라미터 튜닝(자동 머지 조건, smart merge), NSE를 활용한 웜 데이터 계층화, 컬럼 테이블 파티셔닝 전략, 그리고 expensive statement 추적 기반의 워크로드 분석으로 확장해 보길 권합니다. 아래 문서들이 출발점으로 유용합니다.

댓글 0

아직 댓글이 없습니다.