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 바이트에 가까운 저장량을 달성할 수 있습니다.
segmentby와 orderby로 압축 최적화
두 파라미터는 행이 어떻게 배치로 그룹화되고 압축 효율이 어떻게 결정되는지를 좌우합니다:
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)를 기록했습니다. 이는 행 저장소에서 컬럼 저장소 청크로 전환했을 때의 결과입니다.
컬럼스토어가 더 빠른 이유
- Sparse MinMax Index: TimescaleDB는 자동으로
(segmentby_col, _ts_meta_min_1, _ts_meta_max_1)에 인덱스를 구축합니다. 이를 통해 실제 데이터를 읽지 않고도 전체 배치를 제외할 수 있습니다. - Native Filtering: 동일
id를 가진 행이 물리적으로 그룹화되어 있어(id, time)에 대한 대형 B‑tree 인덱스를 사용하지 않아도 바로 해당 세그먼트를 찾을 수 있습니다. - Vectorized Execution: 시간 범위 연산을 1,000행 배치 단위로 처리해 행‑단위 처리 대비 CPU 오버헤드를 크게 줄입니다.