TimescaleDB 시계열 조회 성능 28배 개선
🔍 상세 분석 보기 (EXPLAIN 쿼리 플랜 · 청크 설계 비교 · k6 전체 로그 포함)
→ GitHub 원본 문서 확인하기
요약
| 항목 | Before | After |
|---|---|---|
| P95 (300 RPS) | 7,247 ms | 235 ms |
| 개선율 | — | 약 28배 |
- 문제: 하이퍼테이블 누락 + 인덱스 미적용 → 대용량 시계열 전테이블 스캔
- 개선: 인덱스 최적화 → 하이퍼테이블 적용 → 시간/공간 청크 튜닝
- 환경: 2,600만 행 OHLCV, PostgreSQL 17 + TimescaleDB, k6 부하 테스트
배경
트레이딩 플랫폼의 주 기능은 특정 종목(symb)의 90일치 OHLCV 데이터를 차트로 렌더링하는 것이다. 읽기 위주(append-only) 워크로드이며, 업데이트·삭제는 거의 없고 조회 요청이 집중된다. 목표 SLO: p95 < 300ms
1단계: 인덱스 최적화
문제 확인
초기 테스트에서 8 RPS만으로도 과부하가 발생하여 constant-arrival-rate를 5~7 RPS로 낮춰야 했다. DB 쿼리 성능 저하 및 인덱스 미적용으로 진단하였다.
결과
| 항목 | RPS | P95 | Throughput | FailRate |
|---|---|---|---|---|
| 인덱스 X | 10 | 342.14 ms | 10.01 req/s | 0.00% |
인덱스 O (symb, timestamp) |
10 | 32.06 ms | 10.01 req/s | 0.00% |
→ 인덱스 적용만으로 약 10배 개선
2단계: 하이퍼테이블 적용
문제 확인
덤프/복원 과정에서 TimescaleDB 하이퍼테이블 메타데이터가 누락되어, 테이블이 일반 PostgreSQL 테이블로 운영 중이었다.
SELECT hypertable_name, num_chunks, compression_enabled
FROM timescaledb_information.hypertables;
-- 결과: 조회되지 않음 → 하이퍼테이블 미적용 확인
청크 구조 이해
TimescaleDB 하이퍼테이블은 데이터를 시간(Time) → 공간(Space) 순으로 분할한다.
- 시간 분할:
chunk_time_interval기준으로 timestamp를 주기 단위로 나눔 - 공간 분할:
hash(symb) % num_partitions로 동일 종목이 항상 같은 버킷에 저장
청크 설정 비교 (EXPLAIN 기반, 웜/콜드 캐시 각 5종목 테스트)
| 항목 | 평균 planningTime (콜드) | 평균 executionTime |
|---|---|---|
| 30일 인터벌, 공간 없음 | 49 ms | 3 ms |
| 90일 인터벌, 공간 8 | 31 ms | 3 ms |
| 90일 인터벌, 공간 4 | 23 ms | 3 ms |
- 실행 시간은 인터벌 설정과 무관하게 동일 (0.3~3 ms)
- 조회 범위(90일)와 인터벌을 맞출수록 스캔 청크 수 감소 → cold planning time 단축
- 공간 파티션 수를 줄일수록 같은 이유로 planner 오버헤드 감소
웜 캐시 구간에서는 buffer hit + plan cache로 planning time이 3 ms로 수렴하여, 차이는 콜드 플래닝 구간에서만 유의미하다.
3단계: 하이퍼테이블 전/후 부하 테스트 (동일 인덱스 조건)
| 항목 | RPS | P95 | Throughput | FailRate |
|---|---|---|---|---|
| 일반 PostgreSQL 테이블 | 300 | 7,247.28 ms | 213.40 req/s | 0.00% |
| 하이퍼테이블 (90d, 공간 8) | 300 | 331.99 ms | 300.01 req/s | 0.00% |
| 하이퍼테이블 (90d, 공간 4) | 300 | 235.32 ms | 300.01 req/s | 0.00% |
→ 하이퍼테이블 튜닝 후 약 28배 개선, 공간 파티션 4 기준 SLO 충족
최종 설정 및 선택 이유
| 항목 | 설정 |
|---|---|
| 시간 분할 | 90일 |
| 공간 분할 | 4 |
선택 근거:
- 단순 90일 종목 조회가 주 기능 → 시간 인터벌을 조회 범위와 일치
- append-only 구조로 VACUUM / 인덱스 락 경합 없음
- 하루 1회 배치 쓰기(≈10,000건) → 쓰기 병렬도 중요하지 않음
- 읽기 성능 최적화 우선 → num_partitions=4 (2보다 안정성, 8보다 낮은 planner 오버헤드)
추가: 1D/1W/1M/1Y 조회 전략
| 프레임 | 데이터 소스 |
|---|---|
| 1D | 원본 hypertable 직접 조회 |
| 1W / 1M / 1Y | TimescaleDB Continuous Aggregate (CAGG) materialized view |
- CAGG 인덱스:
(symb, bucket DESC)→ 커서 기반 페이징 최적화 - Refresh: 일봉 배치 완료 후 수동 호출 (
refresh_continuous_aggregate()) — 최근 구간만 갱신하여 DB 부하 최소화
핵심 인사이트
인덱스가 쿼리 경로를 결정하고, 하이퍼테이블이 스캔 범위를 제한한다. 두 조건이 모두 충족되어야 대규모 시계열 조회 성능이 확보된다.
🔍 원본 보고서 보기
→ GitHub 원본 문서 확인하기