TimescaleDB 압축: Hypercore와 컬럼형 스토리지

TimescaleDB는 hypercore 엔진을 활용하여 시계열 데이터에 대해 최대 98%의 압축 비율을 달성합니다. 일반적인 압축과 달리 hypercore는 행‑컬럼 하이브리드 접근 방식을 사용하여 단조성(monotonicity) 및 반복성(repetition)과 같은 시계열 데이터의 수학적 특성에 맞춘 특수 알고리즘을 활용합니다.

Hypercore vs. PostgreSQL TOAST

TimescaleDB 압축은 PostgreSQL 내장 TOAST(The Oversized-Attribute Storage Technique)와 보완 관계에 있으며 대체 관계가 아닙니다. TOAST는 특정 임계값을 초과하는 개별 큰 값(예: 긴 문자열이나 JSONB)을 관리하는 반면, hypercore는 행 간 패턴을 최적화합니다.

Feature TOAST (vanilla PostgreSQL) TimescaleDB hypercore
Design Goal 개별 값 > 2 KB 시계열의 행 간 패턴
Trigger 행이 TOAST_TUPLE_THRESHOLD 초과 청크당 정책(예: 7일 이상 오래된 데이터)
Supported Types 가변 길이만(text, jsonb 등) 모든 데이터 타입
Algorithms pglz, lz4 Delta, Delta-of-Delta, Simple-8b, RLE, XOR 기반, Dictionary
Granularity 값당 배치당(~1,000 행)
Data Structure 값을 불투명 바이트로 취급 숫자 구조와 반복성을 활용
Sensor Floats Ratio ~1.0× 10‑20×
Timestamp Ratio ~1.0× 50‑100×
Text Ratio 2‑3× 5‑10×

컬럼형 압축이 작동하는 방식

Hypercore는 오래된 데이터 청크를 행 기반 포맷(빠른 INSERT에 최적화)에서 컬럼형 포맷으로 변환합니다. 이 과정에서 행은 최대 1,000개의 배치로 그룹화됩니다. 각 배치는 압축 테이블의 단일 행으로 저장되며, 컬럼은 배열 형태로 표현됩니다.

특수 압축 알고리즘

TimescaleDB는 컬럼 데이터 타입에 따라 압축 알고리즘을 선택해 효율성을 극대화합니다:

  • 정수, 타임스탬프, 불리언: 델타 인코딩(값 간 차이 저장), 델타‑오브‑델타(차이의 변화 저장, 규칙적인 간격에서는 0), simple-8b, **런‑길이 인코딩(RLE)**을 조합합니다.
  • 부동소수점(예: 온도/진동): XOR 기반 압축(Gorilla 알고리즘 기반)을 사용합니다. 인접한 부동소수점을 XOR 연산하면 의미 있는 비트만 남겨 긴 0 시퀀스를 무시합니다.
  • JSONB: 두 단계 접근 방식을 사용합니다. 먼저 반복값에 대해 사전을 만든 뒤, 반복이 없을 경우 PostgreSQL TOAST로 폴백합니다.
  • 문자열 및 기타 타입: 사전 압축을 사용하며, 사전 인덱스 자체는 simple-8b와 RLE로 추가 압축됩니다.

예시: Delta와 Run‑Length Encoding

많은 행에 걸쳐 동일한 machine_id가 반복될 경우, RLE는 문자열을 다섯 번 반복하는 대신 MACHINE_001 × 5와 같은 형태로 값 하나와 카운터만 저장합니다. 규칙적인 간격의 타임스탬프는 델타‑오브‑델타 인코딩을 통해 거의 0 바이트에 가까운 저장량을 달성할 수 있습니다.

segmentbyorderby로 압축 최적화

두 파라미터는 행이 어떻게 배치로 그룹화되고 압축 효율이 어떻게 결정되는지를 좌우합니다:

  • segmentby: 배치 내에서 값이 동일한 컬럼을 정의합니다(예: machine_id). 해당 값은 배치당 한 번만 저장됩니다. 쿼리 플래너는 이 메타데이터를 활용해 WHERE 절에 맞지 않는 전체 배치를 건너뛸 수 있습니다.
  • orderby: 배치 내 정렬 순서를 정의합니다(보통 time DESC). 시간 순 정렬은 인접값이 유사할 가능성이 높아 델타와 델타‑오브‑델타 인코딩의 효과를 극대화합니다.

설정 예시:

ALTER TABLE iot_sensor_data SET (
  timescaledb.orderby = 'time DESC',
  timescaledb.segmentby = 'machine_id'
);

베스트 프랙티스: 각 세그먼트는 청크당 최소 100행을 포함해야 하며, 최적 범위는 청크당 100 ~ 10,000개의 고유 segmentby 값입니다.

쿼리 성능에 미치는 영향

대부분의 시계열 워크로드에서 압축은 I/O 요구량을 10‑20배 감소시켜 쿼리 속도를 향상시킵니다.

성능 향상

  • 시간 기반 범위 스캔과 집계(SUM, AVG, MAX).
  • segmentby 컬럼을 이용한 필터링 쿼리.
  • 큰 범위에 대한 시퀀셜 스캔.

성능 트레이드‑오프

  • 단일 행에 대한 포인트 조회는 느려질 수 있습니다.
  • 압축 청크에 대한 UPDATE/DELETE는 압축‑해제 → 수정 → 재압축 사이클이 필요합니다.
  • 고카디널리티 컬럼에 segmentby 필터가 없을 경우 성능 저하가 발생할 수 있습니다.

실제 벤치마크

MQTT 센서 데이터를 사용한 프로덕션 테스트에서, id와 좁은 시간 범위로 포인트 읽기를 수행했을 때 실행 시간 28배 가속(10.2 ms → 0.36 ms)과 압축 비율 42.8배(308 MB → 7.2 MB)를 기록했습니다. 이는 행 저장소에서 컬럼 저장소 청크로 전환했을 때의 결과입니다.

컬럼스토어가 더 빠른 이유

  1. Sparse MinMax Index: TimescaleDB는 자동으로 (segmentby_col, _ts_meta_min_1, _ts_meta_max_1)에 인덱스를 구축합니다. 이를 통해 실제 데이터를 읽지 않고도 전체 배치를 제외할 수 있습니다.
  2. Native Filtering: 동일 id를 가진 행이 물리적으로 그룹화되어 있어 (id, time)에 대한 대형 B‑tree 인덱스를 사용하지 않아도 바로 해당 세그먼트를 찾을 수 있습니다.
  3. Vectorized Execution: 시간 범위 연산을 1,000행 배치 단위로 처리해 행‑단위 처리 대비 CPU 오버헤드를 크게 줄입니다.

Sources