Skip to content

Split View: TimescaleDB 시계열 데이터베이스 운영 가이드: Hypertable 설계부터 연속 집계와 압축 최적화까지

✨ Learn with Quiz
|

TimescaleDB 시계열 데이터베이스 운영 가이드: Hypertable 설계부터 연속 집계와 압축 최적화까지

TimescaleDB Time Series

1. 들어가며

IoT 센서, 애플리케이션 메트릭, 금융 시세, 서버 로그 등 시계열 데이터는 현대 인프라에서 가장 빠르게 성장하는 데이터 유형이다. 이 데이터들은 공통적으로 시간 축을 기준으로 연속적으로 생성되며, INSERT는 대량이지만 UPDATE는 거의 없고, 최근 데이터에 대한 조회가 압도적으로 많다는 특성을 가진다.

PostgreSQL은 범용 관계형 데이터베이스로서 시계열 데이터를 저장할 수는 있지만, 수십억 행 규모에서 시간 범위 쿼리 성능이 급격히 저하된다. 네이티브 파티셔닝을 사용해도 파티션 관리, 압축, 집계 최적화를 직접 구현해야 하는 운영 부담이 크다.

TimescaleDB는 이 문제를 정면으로 해결한다. PostgreSQL의 확장(extension)으로 동작하면서 Hypertable이라는 자동 파티셔닝 메커니즘, 연속 집계(Continuous Aggregates), 네이티브 압축, 자동 데이터 보존 정책 등을 제공한다. 기존 PostgreSQL 쿼리, 인덱스, 조인, 트랜잭션을 그대로 사용하면서도 시계열 워크로드에 특화된 성능을 얻을 수 있다는 점이 가장 큰 장점이다.

이 글에서는 TimescaleDB의 아키텍처부터 Hypertable 설계, 연속 집계, 압축 최적화, 데이터 보존 정책, 멀티노드 구성, 모니터링, 트러블슈팅까지 프로덕션 운영에 필요한 모든 내용을 다룬다.

2. TimescaleDB 아키텍처: Hypertable과 Chunk

Hypertable의 구조

TimescaleDB의 핵심은 Hypertable이다. 사용자에게는 단일 테이블로 보이지만, 내부적으로는 시간 축을 기준으로 자동 분할된 여러 개의 청크(chunk)로 구성된다. 각 청크는 PostgreSQL의 일반 테이블이며, 지정된 시간 간격(기본 7일)에 해당하는 데이터를 저장한다.

사용자 관점:
┌───────────────────────────────────────────┐
sensor_data (Hypertable)SELECT * FROM sensor_data                │
WHERE time > now() - interval '1 hour'└───────────────────────────────────────────┘

내부 구조:
┌────────────────┬────────────────┬────────────────┬────────────────┐
│  _hyper_1_1    │  _hyper_1_2    │  _hyper_1_3    │  _hyper_1_4    │
  (2026-02-15     (2026-02-22     (2026-03-01     (2026-03-07~02-21)~02-28)~03-06)~현재)[압축됨][압축됨][압축 예정][활성]└────────────────┴────────────────┴────────────────┴────────────────┘

이 아키텍처가 제공하는 핵심 이점은 다음과 같다.

  • 자동 파티셔닝: 새 데이터가 도착하면 적절한 청크가 자동으로 생성된다. 수동 파티션 관리가 불필요하다.
  • 쿼리 최적화: 시간 범위 조건이 있는 쿼리는 해당 청크만 스캔한다(chunk exclusion). 불필요한 청크는 아예 접근하지 않는다.
  • 독립적 관리: 각 청크는 독립적으로 압축, 삭제, 이동이 가능하다. 오래된 데이터를 압축하면서 최신 데이터는 고속 INSERT를 유지할 수 있다.
  • 인덱스 효율: 각 청크는 별도의 B-tree 인덱스를 가지므로, 전체 테이블 크기가 아닌 청크 크기에 비례하는 인덱스를 유지한다.

청크 간격(Chunk Interval) 선택 기준

청크 간격은 TimescaleDB 성능에 직접적인 영향을 미치는 핵심 파라미터다. 활성 청크(현재 쓰기가 발생하는 청크)의 인덱스가 메모리에 상주해야 최적의 INSERT 성능을 얻을 수 있다.

권장 기준: 활성 청크 하나의 크기가 가용 메모리의 25% 이하가 되도록 간격을 설정한다.

일일 데이터량메모리권장 청크 간격활성 청크 예상 크기
100MB4GB7일~700MB
1GB8GB1일~1GB
10GB32GB6시간~2.5GB
100GB64GB1시간~4.2GB

3. 설치와 초기 설정

PostgreSQL 위에 TimescaleDB 설치

TimescaleDB는 PostgreSQL 14~17을 지원한다. 2026년 기준 PostgreSQL 16 또는 17과의 조합이 가장 안정적이다.

# Ubuntu/Debian 기준 설치
sudo apt install gnupg postgresql-common apt-transport-https lsb-release wget

# TimescaleDB 패키지 저장소 추가
echo "deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -c -s) main" \
  | sudo tee /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo apt-key add -
sudo apt update

# PostgreSQL 17용 TimescaleDB 설치
sudo apt install timescaledb-2-postgresql-17

# 자동 튜닝 실행 (postgresql.conf 최적화)
sudo timescaledb-tune --yes

# PostgreSQL 재시작
sudo systemctl restart postgresql

timescaledb-tune은 시스템 메모리, CPU 코어 수 등을 분석하여 shared_buffers, work_mem, effective_cache_size, max_worker_processes 등을 자동으로 조정한다. 특히 max_worker_processes는 청크별 병렬 처리에 영향을 미치므로 충분히 확보해야 한다.

-- 데이터베이스에 확장 활성화
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- 설치 확인
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
-- 결과: 2.17.2 (2026년 3월 기준 최신)

핵심 postgresql.conf 설정

# timescaledb-tune이 자동 설정하지만 수동 확인이 필요한 항목
shared_preload_libraries = 'timescaledb'
max_worker_processes = 32          # 병렬 쿼리 + 백그라운드 워커
max_parallel_workers_per_gather = 4
max_parallel_workers = 16
timescaledb.max_background_workers = 16  # 압축, 리오더 등 백그라운드 작업

# 시계열 워크로드 최적화
random_page_cost = 1.1             # SSD 기준
effective_io_concurrency = 200     # SSD 기준
checkpoint_completion_target = 0.9
wal_buffers = 64MB

4. Hypertable 설계 전략

기본 Hypertable 생성

-- 일반 테이블 생성 (PostgreSQL 표준)
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ      NOT NULL,
    sensor_id   INTEGER          NOT NULL,
    location    TEXT             NOT NULL,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    pressure    DOUBLE PRECISION,
    battery     DOUBLE PRECISION
);

-- Hypertable로 변환
SELECT create_hypertable(
    'sensor_data',           -- 테이블 이름
    'time',                  -- 시간 컬럼
    chunk_time_interval => INTERVAL '1 day',  -- 청크 간격
    if_not_exists => TRUE
);

-- 다차원 파티셔닝 (시간 + 공간)
-- 센서 수가 매우 많고 센서별 조회가 빈번한 경우 유용
SELECT create_hypertable(
    'sensor_data',
    'time',
    chunk_time_interval => INTERVAL '1 day',
    partitioning_column => 'sensor_id',  -- 공간 차원 추가
    number_partitions => 4               -- 해시 파티션 수
);

다차원 파티셔닝은 sensor_id = 42 AND time > ... 형태의 쿼리에서 추가적인 청크 제외(chunk exclusion)를 가능하게 한다. 단, 파티션 수를 너무 많이 설정하면 청크 수가 과도하게 늘어나 관리 오버헤드가 증가하므로 주의해야 한다.

인덱스 전략

TimescaleDB는 Hypertable 생성 시 시간 컬럼에 대한 B-tree 인덱스를 자동으로 생성한다. 추가 인덱스는 쿼리 패턴에 맞게 설계한다.

-- 센서별 + 시간 복합 인덱스 (가장 빈번한 쿼리 패턴)
CREATE INDEX idx_sensor_time ON sensor_data (sensor_id, time DESC);

-- 위치별 조회가 빈번한 경우
CREATE INDEX idx_location_time ON sensor_data (location, time DESC);

-- 최근 데이터 위주의 부분 인덱스 (인덱스 크기 절약)
CREATE INDEX idx_recent_temp ON sensor_data (sensor_id, time DESC)
    WHERE time > now() - INTERVAL '30 days';

부분 인덱스(partial index)는 시계열 데이터에서 특히 효과적이다. 대부분의 쿼리가 최근 데이터를 조회하므로, 오래된 데이터에는 인덱스를 유지하지 않아도 된다.

5. 연속 집계(Continuous Aggregates)

개념과 필요성

시계열 데이터 분석에서 가장 흔한 패턴은 시간 단위 집계다. "지난 7일간의 시간별 평균 온도", "월별 센서 가동률" 같은 쿼리를 원본 데이터에 직접 실행하면 수억 행을 스캔해야 한다.

연속 집계(Continuous Aggregates)는 이 문제를 해결하는 TimescaleDB의 핵심 기능이다. PostgreSQL의 Materialized View와 비슷하지만 두 가지 결정적인 차이가 있다.

  • 증분 갱신: 전체 테이블을 다시 집계하지 않고, 마지막 갱신 이후 변경된 데이터만 처리한다.
  • 실시간 결합: 집계된 데이터와 아직 집계되지 않은 최신 데이터를 쿼리 시점에 자동으로 결합하여 항상 최신 결과를 반환한다.

연속 집계 생성과 정책 설정

-- 시간별 집계 Continuous Aggregate 생성
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time)   AS bucket,
    sensor_id,
    location,
    AVG(temperature)              AS avg_temp,
    MIN(temperature)              AS min_temp,
    MAX(temperature)              AS max_temp,
    AVG(humidity)                 AS avg_humidity,
    COUNT(*)                      AS sample_count
FROM sensor_data
GROUP BY bucket, sensor_id, location
WITH NO DATA;  -- 생성 시 즉시 집계하지 않음

-- 자동 갱신 정책 설정
-- 1시간마다 실행, 최근 3시간 데이터를 갱신 대상으로 설정
SELECT add_continuous_aggregate_policy('sensor_hourly',
    start_offset    => INTERVAL '3 hours',   -- 얼마나 과거부터 갱신할 것인가
    end_offset      => INTERVAL '1 hour',    -- 얼마나 최근까지 갱신할 것인가
    schedule_interval => INTERVAL '1 hour'   -- 갱신 주기
);

-- 일별 집계 (시간별 집계 위에 계층적으로 구성 가능)
CREATE MATERIALIZED VIEW sensor_daily
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 day', bucket) AS bucket,
    sensor_id,
    location,
    AVG(avg_temp)                AS avg_temp,
    MIN(min_temp)                AS min_temp,
    MAX(max_temp)                AS max_temp,
    SUM(sample_count)            AS sample_count
FROM sensor_hourly
GROUP BY time_bucket('1 day', bucket), sensor_id, location
WITH NO DATA;

SELECT add_continuous_aggregate_policy('sensor_daily',
    start_offset    => INTERVAL '3 days',
    end_offset      => INTERVAL '1 day',
    schedule_interval => INTERVAL '1 day'
);

**계층적 연속 집계(Hierarchical Continuous Aggregates)**는 TimescaleDB 2.9+에서 지원된다. 원본 Hypertable에서 시간별 집계를 만들고, 시간별 집계에서 일별 집계를 만들면 계산 비용이 크게 줄어든다. 원본 데이터 수십억 행 대신 시간별 집계 수만 행에서 일별 집계를 생성하는 것이다.

실시간 모드와 비실시간 모드

-- 실시간 모드 (기본값) - 항상 최신 데이터 반영
ALTER MATERIALIZED VIEW sensor_hourly
    SET (timescaledb.materialized_only = false);

-- 비실시간 모드 - 마지막 갱신 시점까지의 데이터만 반환 (더 빠름)
ALTER MATERIALIZED VIEW sensor_hourly
    SET (timescaledb.materialized_only = true);

실시간 모드는 집계되지 않은 최신 데이터를 쿼리 시점에 on-the-fly로 집계하므로 약간의 성능 비용이 있다. 대시보드에서 1분 이내의 정확성이 필요하지 않다면 비실시간 모드가 더 효율적이다.

6. 네이티브 압축

압축 아키텍처

TimescaleDB의 네이티브 압축은 청크 단위로 동작한다. 압축된 청크는 행 지향 저장에서 컬럼 지향 저장으로 변환되며, 데이터 타입에 따라 최적의 압축 알고리즘이 자동 적용된다.

압축 효과: 일반적인 시계열 데이터에서 9095%의 스토리지 절감이 가능하다. 이는 원본 100GB 데이터가 510GB로 줄어드는 수준이다.

-- 압축 설정 활성화
ALTER TABLE sensor_data SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id, location',  -- 세그먼트 기준
    timescaledb.compress_orderby = 'time DESC'               -- 정렬 기준
);

-- 자동 압축 정책: 7일 이상 된 청크를 자동 압축
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');

-- 수동 압축 (특정 청크 즉시 압축)
SELECT compress_chunk(c.chunk_name)
FROM timescaledb_information.chunks c
WHERE c.hypertable_name = 'sensor_data'
  AND c.range_end < now() - INTERVAL '7 days'
  AND NOT c.is_compressed;

-- 압축 상태 확인
SELECT
    chunk_name,
    range_start,
    range_end,
    is_compressed,
    pg_size_pretty(before_compression_total_bytes) AS before_size,
    pg_size_pretty(after_compression_total_bytes)  AS after_size,
    ROUND(
        (1 - after_compression_total_bytes::numeric
             / NULLIF(before_compression_total_bytes, 0)) * 100, 1
    ) AS compression_ratio_pct
FROM timescaledb_information.compressed_chunk_stats
WHERE hypertable_name = 'sensor_data'
ORDER BY range_end DESC
LIMIT 10;

compress_segmentby 설계

compress_segmentby는 압축 성능의 핵심이다. 이 컬럼은 압축된 청크 내에서 데이터를 그룹화하는 기준이 된다. 압축된 데이터에 대한 쿼리가 WHERE sensor_id = 42처럼 세그먼트 키로 필터링하면, 해당 세그먼트만 해제하면 되므로 쿼리 성능이 크게 향상된다.

설계 원칙:

  • segmentby에는 WHERE 조건이나 GROUP BY에 자주 사용되는 컬럼을 지정한다.
  • 카디널리티가 너무 높은 컬럼(예: user_id가 수백만 개)은 피한다. 세그먼트 수가 과도하게 늘어나 압축률이 떨어진다.
  • 카디널리티가 너무 낮은 컬럼(예: status가 3개)도 단독으로는 비효율적이다.
  • 일반적으로 수백~수만 개의 고유 값을 가진 컬럼이 적합하다.

7. 데이터 보존 정책(Retention Policy)

시계열 데이터는 시간이 지남에 따라 가치가 감소하는 경우가 많다. 오래된 원본 데이터는 삭제하되, 집계된 데이터는 더 오래 보존하는 계층적 보존 전략이 효과적이다.

-- 90일 이상 된 원본 데이터 자동 삭제
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');

-- 시간별 집계는 1년 보존
SELECT add_retention_policy('sensor_hourly', INTERVAL '1 year');

-- 일별 집계는 3년 보존
SELECT add_retention_policy('sensor_daily', INTERVAL '3 years');

-- 보존 정책 확인
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';

-- 수동으로 특정 시점 이전 데이터 즉시 삭제
SELECT drop_chunks('sensor_data', older_than => INTERVAL '90 days');

이 계층적 보존 전략의 효과를 정리하면 다음과 같다.

데이터 계층보존 기간해상도주 용도
원본 데이터90일초 단위실시간 모니터링, 디버깅
시간별 집계1년시간 단위트렌드 분석, 대시보드
일별 집계3년일 단위장기 리포트, 용량 계획

8. 쿼리 최적화

시계열 전용 함수 활용

TimescaleDB는 시계열 분석에 특화된 하이퍼함수(Hyperfunctions)를 제공한다.

-- time_bucket: 시간 단위 그룹화 (가장 핵심적인 함수)
SELECT
    time_bucket('15 minutes', time) AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp,
    percentile_cont(0.95) WITHIN GROUP (ORDER BY temperature) AS p95_temp
FROM sensor_data
WHERE time > now() - INTERVAL '6 hours'
    AND sensor_id = 42
GROUP BY bucket, sensor_id
ORDER BY bucket DESC;

-- time_bucket_gapfill: 데이터가 없는 시간대도 채워서 반환
SELECT
    time_bucket_gapfill('1 hour', time) AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp,
    COALESCE(AVG(temperature), locf(AVG(temperature))) AS filled_temp
FROM sensor_data
WHERE time > now() - INTERVAL '24 hours'
    AND sensor_id = 42
GROUP BY bucket, sensor_id
ORDER BY bucket;

-- first/last: 시간 기준 첫 번째/마지막 값
SELECT
    time_bucket('1 hour', time) AS bucket,
    sensor_id,
    first(temperature, time) AS opening_temp,
    last(temperature, time)  AS closing_temp,
    MAX(temperature) - MIN(temperature) AS temp_range
FROM sensor_data
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id;

쿼리 성능 최적화 체크리스트

  • 시간 범위 조건을 반드시 포함하라: WHERE time > now() - INTERVAL '1 day' 없이 전체 테이블을 스캔하면 청크 제외(chunk exclusion)가 동작하지 않는다.
  • 세그먼트 키 필터를 함께 사용하라: 압축된 데이터에서 WHERE sensor_id = 42를 추가하면 해당 세그먼트만 해제한다.
  • EXPLAIN ANALYZE로 청크 제외를 확인하라: 실행 계획에서 스캔된 청크 수가 예상보다 많다면 쿼리 조건이나 청크 간격을 재검토한다.
  • 연속 집계를 활용하라: 대시보드 쿼리는 가능하면 원본 데이터 대신 연속 집계를 조회하도록 설계한다.
-- 쿼리 실행 계획에서 청크 제외 확인
EXPLAIN ANALYZE
SELECT AVG(temperature)
FROM sensor_data
WHERE time > now() - INTERVAL '2 hours'
    AND sensor_id = 42;

-- 결과에서 "Chunks excluded" 항목 확인
-- Append (actual rows=...)
--   -> Index Scan on _hyper_1_42_chunk (actual rows=...)
--        Index Cond: (time > ...)
--   -> (N chunks excluded)  <-- 이 부분이 핵심

9. 멀티노드 구성

TimescaleDB는 단일 노드에서도 수 TB 규모의 시계열 데이터를 처리할 수 있지만, 더 큰 규모에서는 멀티노드 구성이 필요하다. 2025년부터 TimescaleDB는 멀티노드 아키텍처를 Timescale Cloud의 서비스 기반 확장으로 전환하는 방향으로 발전하고 있다.

셀프호스팅 환경에서의 수평 확장 전략은 다음과 같다.

  • 읽기 확장: PostgreSQL의 스트리밍 복제를 활용하여 읽기 전용 레플리카를 추가한다. 대시보드 쿼리를 레플리카로 분산한다.
  • 쓰기 확장: 센서 ID나 지역 같은 논리적 기준으로 데이터를 분할하여 여러 TimescaleDB 인스턴스에 분산한다. 애플리케이션 레벨에서 라우팅하거나 Citus와 결합하는 방법이 있다.
  • Timescale Cloud: 관리형 서비스를 사용하면 동적 리소스 스케일링, 자동 복제, 포인트 인 타임 복구 등이 기본 제공된다.

10. 모니터링

핵심 모니터링 쿼리

-- 1. Hypertable별 전체 크기 확인
SELECT
    hypertable_name,
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)) AS total_size,
    pg_size_pretty(pg_total_relation_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)
        - pg_relation_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)) AS index_size,
    num_chunks,
    compression_enabled
FROM timescaledb_information.hypertables
ORDER BY hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass) DESC;

-- 2. 청크별 상세 정보 (크기, 압축 여부, 행 수)
SELECT
    chunk_name,
    range_start,
    range_end,
    is_compressed,
    pg_size_pretty(
        pg_total_relation_size(format('%I.%I', chunk_schema, chunk_name)::regclass)
    ) AS chunk_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_end DESC
LIMIT 20;

-- 3. 백그라운드 작업(Jobs) 상태 모니터링
SELECT
    job_id,
    application_name,
    proc_name,
    hypertable_name,
    schedule_interval,
    last_run_status,
    last_run_started_at,
    last_run_duration,
    next_start,
    total_successes,
    total_failures
FROM timescaledb_information.job_stats js
JOIN timescaledb_information.jobs j USING (job_id)
ORDER BY last_run_started_at DESC;

-- 4. 압축 효율 종합 리포트
SELECT
    hypertable_name,
    pg_size_pretty(SUM(before_compression_total_bytes)) AS before_compression,
    pg_size_pretty(SUM(after_compression_total_bytes))  AS after_compression,
    ROUND(
        (1 - SUM(after_compression_total_bytes)::numeric
             / NULLIF(SUM(before_compression_total_bytes), 0)) * 100, 1
    ) AS savings_pct,
    COUNT(*) AS compressed_chunks
FROM timescaledb_information.compressed_chunk_stats
GROUP BY hypertable_name;

Prometheus + Grafana 연동

TimescaleDB는 pg_prometheus 어댑터 또는 Promscale(현재 deprecated, 후속은 Promscale의 기능을 흡수한 Timescale Cloud)을 통해 Prometheus 메트릭을 직접 저장할 수도 있다. 역으로 TimescaleDB 자체의 메트릭을 Prometheus로 수집하려면 postgres_exporter를 사용한다.

11. 시계열 데이터베이스 비교

항목TimescaleDBInfluxDB (3.x)ClickHouse
기반 기술PostgreSQL 확장Apache Arrow + DataFusion자체 엔진 (C++)
쿼리 언어표준 SQL (완전 호환)SQL (InfluxQL 대체), Flight SQLSQL (자체 방언)
JOIN 지원완전 지원 (PostgreSQL 기능)제한적지원 (대형 JOIN 주의)
압축률90~95% (컬럼 지향 변환)높음 (Parquet 기반)90~95% (컬럼 지향 네이티브)
연속 집계네이티브 지원 (증분 갱신)태스크 기반 집계Materialized View (INSERT 트리거)
데이터 보존 정책네이티브 (자동 청크 삭제)버킷 기반 보존TTL (파티션/행 단위)
트랜잭션 지원완전 ACID (PostgreSQL)미지원미지원
학습 곡선낮음 (PostgreSQL 사용자라면)중간 (새 아키텍처 전환 중)중간 (고유 개념 다수)
에코시스템PostgreSQL 전체 (pg 확장, ORM)Telegraf, Kapacitor독립적 에코시스템
주 사용 사례IoT, 메트릭, PostgreSQL 확장DevOps 메트릭, IoT대규모 분석, 로그
라이선스Apache 2.0 (Community)MIT / Apache 2.0Apache 2.0

선택 가이드:

  • 이미 PostgreSQL을 운영 중이고, SQL 호환성과 트랜잭션이 중요하다면: TimescaleDB가 최선이다. 별도의 데이터베이스를 추가로 운영할 필요가 없다.
  • 순수 메트릭/로그 수집 파이프라인이 핵심이고 Telegraf 에코시스템을 활용하고 싶다면: InfluxDB가 적합하다.
  • 수십 TB 이상의 분석 워크로드에서 서브초 응답이 필요하다면: ClickHouse가 최적이다. 단, ACID 트랜잭션은 포기해야 한다.

12. 트러블슈팅

문제 1: INSERT 성능 저하

증상: INSERT 지연시간이 점차 증가한다. 특히 청크 경계(새 청크 생성 시점)에서 스파이크가 발생한다.

원인과 해결:

  • 청크 간격이 너무 짧아 빈번한 청크 생성이 발생하는 경우: 청크 간격을 늘린다.
  • 인덱스가 과도한 경우: 불필요한 인덱스를 제거한다. 시계열 데이터에서 인덱스 하나를 추가하면 INSERT 성능이 10~20% 저하될 수 있다.
  • WAL 쓰기 병목: wal_buffers, max_wal_size를 증가시킨다.

문제 2: 압축된 청크에서 느린 쿼리

증상: 압축된 데이터를 조회할 때 비압축 데이터보다 오히려 느리다.

원인과 해결:

  • compress_segmentby가 쿼리 패턴과 맞지 않는 경우: 세그먼트 키로 필터링하지 않는 쿼리는 모든 세그먼트를 해제해야 한다. 쿼리 패턴에 맞게 segmentby 설정을 변경한다(기존 압축 데이터는 해제 후 재압축 필요).
  • 압축된 청크에 대한 ORDER BYcompress_orderby와 다른 경우: 정렬 방향을 맞춘다.

문제 3: 연속 집계 갱신 실패

증상: timescaledb_information.job_stats에서 연속 집계 작업의 last_run_statusFailed로 표시된다.

원인과 해결:

-- 실패한 작업 상세 확인
SELECT
    job_id,
    proc_name,
    last_run_status,
    last_run_started_at,
    last_run_duration,
    config
FROM timescaledb_information.job_stats js
JOIN timescaledb_information.jobs j USING (job_id)
WHERE last_run_status = 'Failed';

-- 작업 로그에서 오류 메시지 확인 (PostgreSQL 로그)
-- 일반적인 원인: 메모리 부족(work_mem), 디스크 공간 부족, 락 경합

-- 수동으로 연속 집계 갱신 실행 (디버깅 목적)
CALL refresh_continuous_aggregate('sensor_hourly',
    now() - INTERVAL '3 hours',
    now() - INTERVAL '1 hour'
);

문제 4: 디스크 공간 폭증

증상: 디스크 사용량이 예상보다 빠르게 증가하여 서비스에 영향을 준다.

해결 순서:

  1. 압축 정책이 정상 동작하는지 확인한다.
  2. 보존 정책이 설정되어 있는지 확인한다.
  3. 비압축 청크 중 오래된 것을 수동으로 압축한다.
  4. 불필요한 오래된 청크를 즉시 삭제한다.

13. 운영 시 주의사항

압축된 데이터의 DML 제한

압축된 청크에는 INSERT, UPDATE, DELETE가 직접 실행될 수 없다. 압축된 시간 범위에 늦게 도착한 데이터(late-arriving data)를 처리하려면 해당 청크를 먼저 해제해야 한다.

-- 특정 청크 압축 해제
SELECT decompress_chunk('_timescaledb_internal._hyper_1_3_chunk');

-- 데이터 INSERT/UPDATE 수행 후 다시 압축
SELECT compress_chunk('_timescaledb_internal._hyper_1_3_chunk');

늦게 도착하는 데이터가 빈번한 경우, 압축 대상에서 최근 N일을 제외하는 오프셋을 충분히 크게 설정해야 한다.

마이그레이션 시 주의사항

  • TimescaleDB 메이저 버전 업그레이드(예: 2.x -> 3.x) 시에는 반드시 공식 마이그레이션 가이드를 따른다.
  • PostgreSQL 메이저 버전 업그레이드와 TimescaleDB 업그레이드는 동시에 하지 않는다. 하나씩 순차적으로 진행한다.
  • 업그레이드 전에 pg_dump로 스키마 백업을 수행하고, 가능하면 테스트 환경에서 먼저 검증한다.

커넥션 풀링

TimescaleDB는 내부적으로 백그라운드 워커를 사용하므로 max_connections에 여유를 두어야 한다. PgBouncer를 사용할 때는 transaction 모드를 사용하고, prepared statement 호환성에 주의한다.

14. 실패 사례와 복구

사례 1: 청크 간격 미설정으로 인한 성능 저하

한 팀이 기본 청크 간격(7일)을 사용하면서 일 100GB의 센서 데이터를 적재했다. 활성 청크가 700GB에 달해 메모리(32GB)의 20배를 초과했고, INSERT 지연이 수 초까지 증가했다.

복구: 청크 간격을 1시간으로 변경하고 기존 데이터를 마이그레이션했다. 청크 간격 변경은 새로 생성되는 청크부터 적용되므로, 기존 데이터는 새 Hypertable로 이동해야 했다.

-- 청크 간격 변경 (새 청크부터 적용)
SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 hour');

사례 2: 압축 segmentby 미설정으로 인한 쿼리 성능 악화

압축을 활성화하면서 compress_segmentby를 지정하지 않았다. 모든 데이터가 단일 세그먼트로 압축되어, WHERE sensor_id = 42 같은 필터 쿼리가 전체 청크를 해제해야 했다. 압축 전보다 쿼리가 3배 느려졌다.

복구: 모든 압축된 청크를 해제하고, compress_segmentby를 설정한 후 재압축했다. 대규모 데이터에서는 이 과정이 수 시간이 걸릴 수 있으므로, 처음부터 올바르게 설정하는 것이 중요하다.

사례 3: 보존 정책 없이 운영하여 스토리지 고갈

3년간 보존 정책 없이 IoT 센서 데이터를 적재했다. 전체 데이터 크기가 15TB에 달했고, 분석 쿼리는 대부분 최근 30일 데이터만 사용했다. 스토리지 비용이 월 수천 달러에 달했다.

복구: 연속 집계를 생성하여 과거 데이터의 시간별/일별 집계를 먼저 확보한 후, 원본 데이터에 90일 보존 정책을 적용했다. 스토리지가 15TB에서 800GB로 감소했다.

15. 프로덕션 체크리스트

Hypertable 설계

  • 청크 간격이 일일 데이터량과 메모리에 맞게 설정되었는가
  • 다차원 파티셔닝이 필요한지 검토했는가 (고유 디바이스/센서 수가 매우 많은 경우)
  • 시간 컬럼의 타임존 처리 방식이 통일되었는가 (TIMESTAMPTZ 권장)

인덱스

  • 주요 쿼리 패턴에 맞는 복합 인덱스가 존재하는가
  • 불필요한 인덱스가 INSERT 성능을 저하시키고 있지 않은가
  • 부분 인덱스를 활용하여 인덱스 크기를 최적화했는가

압축

  • compress_segmentby가 쿼리 패턴과 일치하는가
  • 압축 정책의 시간 오프셋이 늦게 도착하는 데이터를 고려했는가
  • 압축률이 기대 수준(80% 이상)을 달성하고 있는가

연속 집계

  • 대시보드/리포트 쿼리에 연속 집계가 활용되고 있는가
  • 갱신 주기와 오프셋이 데이터 지연 특성에 맞게 설정되었는가
  • 계층적 집계(시간별 -> 일별)가 구성되었는가

데이터 보존

  • 원본 데이터의 보존 기간이 설정되었는가
  • 집계 데이터의 보존 기간이 원본보다 길게 설정되었는가
  • 보존 정책 작업이 정상 실행되고 있는가 (job_stats 확인)

모니터링

  • 디스크 사용량 모니터링과 알림이 설정되었는가
  • 백그라운드 작업(압축, 보존, 집계) 실행 상태를 모니터링하는가
  • 슬로우 쿼리 로깅이 활성화되었는가 (log_min_duration_statement)
  • 청크 수가 과도하게 증가하고 있지 않은가

백업과 복구

  • pg_dump 또는 pg_basebackup으로 정기 백업이 설정되었는가
  • 복구 테스트를 정기적으로 수행했는가
  • WAL 아카이빙으로 포인트 인 타임 복구가 가능한가

16. 참고자료

TimescaleDB Time Series Database Operation Guide: From Hypertable Design to Continuous Aggregation and Compression Optimization

TimescaleDB Time Series

1. Introduction

Time series data, including IoT sensors, application metrics, financial quotes, and server logs, is the fastest-growing data type in modern infrastructure. These data are commonly created continuously based on the time axis, and have the characteristics of a large amount of INSERTs but almost no UPDATEs, and an overwhelming number of searches for recent data.

PostgreSQL is a general-purpose relational database that can store time series data, but time range query performance degrades rapidly at scale of billions of rows. Even if native partitioning is used, there is a significant operational burden of having to manually implement partition management, compression, and aggregation optimization.

TimescaleDB addresses this problem head-on. It operates as an extension of PostgreSQL and provides an automatic partitioning mechanism called Hypertable, continuous aggregates, native compression, and automatic data retention policy. The biggest advantage is that you can obtain performance specialized for time series workloads while using existing PostgreSQL queries, indexes, joins, and transactions.

This article covers everything needed for production operations, from TimescaleDB's architecture to Hypertable design, continuous aggregation, compression optimization, data retention policy, multi-node configuration, monitoring, and troubleshooting.

2. TimescaleDB Architecture: Hypertable and Chunk

Structure of Hypertable

The core of TimescaleDB is Hypertable. It appears to be a single table to the user, but internally it is composed of multiple chunks that are automatically divided based on the time axis. Each chunk is a regular table in PostgreSQL and stores data corresponding to a specified time interval (default 7 days).

사용자 관점:
┌───────────────────────────────────────────┐
sensor_data (Hypertable)SELECT \* FROM sensor_data │
WHERE time > now() - interval '1 hour'└───────────────────────────────────────────┘

내부 구조:
┌────────────────┬────────────────┬────────────────┬────────────────┐
│ \_hyper_1_1 │ \_hyper_1_2 │ \_hyper_1_3 │ \_hyper_1_4 │
 (2026-02-15  (2026-02-22  (2026-03-01  (2026-03-07~02-21)~02-28)~03-06)~현재)[압축됨][압축됨][압축 예정][활성]└────────────────┴────────────────┴────────────────┴────────────────┘

The key benefits provided by this architecture are:

  • Automatic Partitioning: When new data arrives, appropriate chunks are automatically created. Manual partition management is unnecessary.
  • Query optimization: Queries with time range conditions scan only the corresponding chunks (chunk exclusion). Unnecessary chunks are not accessed at all.
  • Independent management: Each chunk can be compressed, deleted, and moved independently. High-speed INSERT of newer data can be maintained while compressing old data.
  • Index efficiency: Since each chunk has a separate B-tree index, an index is maintained that is proportional to the chunk size, not the overall table size.

Chunk Interval Selection Criteria

Chunk interval is a key parameter that directly affects TimescaleDB performance. For optimal INSERT performance, the index of the active chunk (the chunk where writes are currently occurring) must reside in memory.

Recommended standard: Set the interval so that the size of one active chunk is 25% or less of available memory.

Daily data volumememoryRecommended Chunk IntervalActive chunk expected size
100 MB4GB7 days~700MB
1GB8GB1 day~1GB
10GB32GB6 hours~2.5GB
100GB64GB1 hour~4.2GB

3. Installation and initial setup

Install TimescaleDB on top of PostgreSQL

TimescaleDB supports PostgreSQL 14~17. As of 2026, combination with PostgreSQL 16 or 17 is the most stable.

# Ubuntu/Debian 기준 설치
sudo apt install gnupg postgresql-common apt-transport-https lsb-release wget

# TimescaleDB 패키지 저장소 추가
echo "deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -c -s) main" \
  | sudo tee /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo apt-key add -
sudo apt update

# PostgreSQL 17용 TimescaleDB 설치
sudo apt install timescaledb-2-postgresql-17

# 자동 튜닝 실행 (postgresql.conf 최적화)
sudo timescaledb-tune --yes

# PostgreSQL 재시작
sudo systemctl restart postgresql

timescaledb-tuneAnalyzes system memory, number of CPU cores, etc.shared_buffers, work_mem, effective_cache_size, max_worker_processesAutomatically adjusts etc. especiallymax_worker_processesSince it affects parallel processing for each chunk, it must be sufficiently secured.

-- 데이터베이스에 확장 활성화
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- 설치 확인
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
-- 결과: 2.17.2 (2026년 3월 기준 최신)
`### Core postgresql.conf settings`ini

# timescaledb-tune이 자동 설정하지만 수동 확인이 필요한 항목

shared_preload_libraries = 'timescaledb'
max_worker_processes = 32 # 병렬 쿼리 + 백그라운드 워커
max_parallel_workers_per_gather = 4
max_parallel_workers = 16
timescaledb.max_background_workers = 16 # 압축, 리오더 등 백그라운드 작업

# 시계열 워크로드 최적화

random_page_cost = 1.1 # SSD 기준
effective_io_concurrency = 200 # SSD 기준
checkpoint_completion_target = 0.9
wal_buffers = 64MB

4. Hypertable design strategy

Create a basic Hypertable

-- 일반 테이블 생성 (PostgreSQL 표준)
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ      NOT NULL,
    sensor_id   INTEGER          NOT NULL,
    location    TEXT             NOT NULL,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    pressure    DOUBLE PRECISION,
    battery     DOUBLE PRECISION
);

-- Hypertable로 변환
SELECT create_hypertable(
    'sensor_data',           -- 테이블 이름
    'time',                  -- 시간 컬럼
    chunk_time_interval => INTERVAL '1 day',  -- 청크 간격
    if_not_exists => TRUE
);

-- 다차원 파티셔닝 (시간 + 공간)
-- 센서 수가 매우 많고 센서별 조회가 빈번한 경우 유용
SELECT create_hypertable(
    'sensor_data',
    'time',
    chunk_time_interval => INTERVAL '1 day',
    partitioning_column => 'sensor_id',  -- 공간 차원 추가
    number_partitions => 4               -- 해시 파티션 수
);

Multidimensional partitioning issensor_id = 42 AND time > ...Enables additional chunk exclusion in queries of the form However, caution must be taken because if the number of partitions is set too high, the number of chunks will increase excessively and management overhead will increase.

Index Strategy

TimescaleDB automatically creates a B-tree index for the time column when creating a hypertable. Additional indexes are designed to suit the query pattern.

-- 센서별 + 시간 복합 인덱스 (가장 빈번한 쿼리 패턴)
CREATE INDEX idx_sensor_time ON sensor_data (sensor_id, time DESC);

-- 위치별 조회가 빈번한 경우
CREATE INDEX idx_location_time ON sensor_data (location, time DESC);

-- 최근 데이터 위주의 부분 인덱스 (인덱스 크기 절약)
CREATE INDEX idx_recent_temp ON sensor_data (sensor_id, time DESC)
    WHERE time > now() - INTERVAL '30 days';

Partial indices are especially effective for time series data. Since most queries retrieve recent data, there is no need to maintain indexes on old data.

5. Continuous Aggregates

Concept and necessity

The most common pattern in time series data analysis is hourly aggregation. If queries such as “hourly average temperature for the past 7 days” or “monthly sensor utilization rate” are run directly on the original data, hundreds of millions of rows must be scanned.

Continuous Aggregates are a key feature of TimescaleDB that solves this problem. It is similar to PostgreSQL's Materialized View, but there are two crucial differences.

  • Incremental update: Processes only data that has changed since the last update, without reaggregating the entire table.
  • Real-time combining: Automatically combines aggregated data and the latest data that has not yet been aggregated at the time of query to always return the latest results.

Creating continuous aggregates and setting policies

-- 시간별 집계 Continuous Aggregate 생성
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time)   AS bucket,
    sensor_id,
    location,
    AVG(temperature)              AS avg_temp,
    MIN(temperature)              AS min_temp,
    MAX(temperature)              AS max_temp,
    AVG(humidity)                 AS avg_humidity,
    COUNT(*)                      AS sample_count
FROM sensor_data
GROUP BY bucket, sensor_id, location
WITH NO DATA;  -- 생성 시 즉시 집계하지 않음

-- 자동 갱신 정책 설정
-- 1시간마다 실행, 최근 3시간 데이터를 갱신 대상으로 설정
SELECT add_continuous_aggregate_policy('sensor_hourly',
    start_offset    => INTERVAL '3 hours',   -- 얼마나 과거부터 갱신할 것인가
    end_offset      => INTERVAL '1 hour',    -- 얼마나 최근까지 갱신할 것인가
    schedule_interval => INTERVAL '1 hour'   -- 갱신 주기
);

-- 일별 집계 (시간별 집계 위에 계층적으로 구성 가능)
CREATE MATERIALIZED VIEW sensor_daily
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 day', bucket) AS bucket,
    sensor_id,
    location,
    AVG(avg_temp)                AS avg_temp,
    MIN(min_temp)                AS min_temp,
    MAX(max_temp)                AS max_temp,
    SUM(sample_count)            AS sample_count
FROM sensor_hourly
GROUP BY time_bucket('1 day', bucket), sensor_id, location
WITH NO DATA;

SELECT add_continuous_aggregate_policy('sensor_daily',
    start_offset    => INTERVAL '3 days',
    end_offset      => INTERVAL '1 day',
    schedule_interval => INTERVAL '1 day'
);

Hierarchical Continuous Aggregates is supported in TimescaleDB 2.9+. Creating hourly aggregations from the original hypertable and daily aggregations from hourly aggregations significantly reduces the computational cost. Instead of billions of rows of original data, daily aggregates are created from tens of thousands of hourly aggregate rows.

Real-time mode and non-real-time mode

-- 실시간 모드 (기본값) - 항상 최신 데이터 반영
ALTER MATERIALIZED VIEW sensor_hourly
    SET (timescaledb.materialized_only = false);

-- 비실시간 모드 - 마지막 갱신 시점까지의 데이터만 반환 (더 빠름)
ALTER MATERIALIZED VIEW sensor_hourly
    SET (timescaledb.materialized_only = true);

Real-time mode has a slight performance cost because it aggregates up-to-date, unaggregated data on-the-fly at query time. If your dashboard does not require sub-minute accuracy, non-real-time mode is more efficient.

6. Native compression

Compression architecture

TimescaleDB's native compression operates on a chunk basis. Compressed chunks are converted from row-oriented storage to column-oriented storage, and the optimal compression algorithm is automatically applied depending on the data type.

Compression effect: Storage savings of 90-95% are possible for general time series data. This is the level where the original 100GB data is reduced to 5~10GB.

-- 압축 설정 활성화
ALTER TABLE sensor_data SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id, location',  -- 세그먼트 기준
    timescaledb.compress_orderby = 'time DESC'               -- 정렬 기준
);

-- 자동 압축 정책: 7일 이상 된 청크를 자동 압축
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');

-- 수동 압축 (특정 청크 즉시 압축)
SELECT compress_chunk(c.chunk_name)
FROM timescaledb_information.chunks c
WHERE c.hypertable_name = 'sensor_data'
  AND c.range_end < now() - INTERVAL '7 days'
  AND NOT c.is_compressed;

-- 압축 상태 확인
SELECT
    chunk_name,
    range_start,
    range_end,
    is_compressed,
    pg_size_pretty(before_compression_total_bytes) AS before_size,
    pg_size_pretty(after_compression_total_bytes)  AS after_size,
    ROUND(
        (1 - after_compression_total_bytes::numeric
             / NULLIF(before_compression_total_bytes, 0)) * 100, 1
    ) AS compression_ratio_pct
FROM timescaledb_information.compressed_chunk_stats
WHERE hypertable_name = 'sensor_data'
ORDER BY range_end DESC
LIMIT 10;

compress_segmentby designcompress_segmentbyis the key to compression performance. This column serves as the basis for grouping data within the compressed chunk. Queries on compressed dataWHERE sensor_id = 42If you filter by segment key, query performance is greatly improved because only the corresponding segment needs to be released.

Design Principles:

-segmentbySpecifies columns frequently used in WHERE conditions or GROUP BY.

  • Avoid columns with too high cardinality (e.g., millions of user_ids). If the number of segments increases excessively, the compression rate decreases.
  • Columns with too low cardinality (e.g., 3 status values) are also inefficient when used alone.
  • Generally, columns with hundreds to tens of thousands of unique values ​​are suitable.

7. Data Retention Policy

Time series data often loses value over time. A hierarchical preservation strategy that deletes old original data but preserves aggregated data longer is effective.

-- 90일 이상 된 원본 데이터 자동 삭제
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');

-- 시간별 집계는 1년 보존
SELECT add_retention_policy('sensor_hourly', INTERVAL '1 year');

-- 일별 집계는 3년 보존
SELECT add_retention_policy('sensor_daily', INTERVAL '3 years');

-- 보존 정책 확인
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';

-- 수동으로 특정 시점 이전 데이터 즉시 삭제
SELECT drop_chunks('sensor_data', older_than => INTERVAL '90 days');

The effects of this hierarchical preservation strategy are summarized as follows.

data layerRetention periodResolutionMain use
Original data90 dayssecondsReal-time monitoring, debugging
Hourly tally1 yeartime unitTrend analysis, dashboard
Daily tally3 yearsday unitLong Term Reports, Capacity Planning

8. Query optimization

Utilizing time series-specific functions

TimescaleDB provides hyperfunctions specialized for time series analysis.

-- time_bucket: 시간 단위 그룹화 (가장 핵심적인 함수)
SELECT
    time_bucket('15 minutes', time) AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp,
    percentile_cont(0.95) WITHIN GROUP (ORDER BY temperature) AS p95_temp
FROM sensor_data
WHERE time > now() - INTERVAL '6 hours'
    AND sensor_id = 42
GROUP BY bucket, sensor_id
ORDER BY bucket DESC;

-- time_bucket_gapfill: 데이터가 없는 시간대도 채워서 반환
SELECT
    time_bucket_gapfill('1 hour', time) AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp,
    COALESCE(AVG(temperature), locf(AVG(temperature))) AS filled_temp
FROM sensor_data
WHERE time > now() - INTERVAL '24 hours'
    AND sensor_id = 42
GROUP BY bucket, sensor_id
ORDER BY bucket;

-- first/last: 시간 기준 첫 번째/마지막 값
SELECT
    time_bucket('1 hour', time) AS bucket,
    sensor_id,
    first(temperature, time) AS opening_temp,
    last(temperature, time)  AS closing_temp,
    MAX(temperature) - MIN(temperature) AS temp_range
FROM sensor_data
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id;

Query performance optimization checklist

  • MUST INCLUDE TIME RANGE CONDITIONS:WHERE time > now() - INTERVAL '1 day'If you scan the entire table without it, chunk exclusion will not work.
  • Use segment key filters together: on compressed dataWHERE sensor_id = 42If added, only the corresponding segment is released.
  • Check chunk exclusion with EXPLAIN ANALYZE: If the number of chunks scanned in the execution plan is more than expected, reexamine the query conditions or chunk intervals.
  • Use continuous aggregation: Dashboard queries are designed to query continuous aggregation instead of original data whenever possible.
-- 쿼리 실행 계획에서 청크 제외 확인
EXPLAIN ANALYZE
SELECT AVG(temperature)
FROM sensor_data
WHERE time > now() - INTERVAL '2 hours'
    AND sensor_id = 42;

-- 결과에서 "Chunks excluded" 항목 확인
-- Append (actual rows=...)
--   -> Index Scan on _hyper_1_42_chunk (actual rows=...)
--        Index Cond: (time > ...)
--   -> (N chunks excluded)  <-- 이 부분이 핵심

9. Multi-node configuration

TimescaleDB can process several TB of time series data on a single node, but a multi-node configuration is required for larger scales. Starting in 2025, TimescaleDB is evolving to transform its multi-node architecture into a service-based extension of Timescale Cloud.

The horizontal expansion strategy in a self-hosting environment is as follows.

  • Read Expansion: Add read-only replicas using PostgreSQL's streaming replication. Distribute dashboard queries to replicas.
  • Write Scaling: Split data by logical criteria such as sensor ID or region and distribute it to multiple TimescaleDB instances. There are ways to route at the application level or combine it with Citus.
  • Timescale Cloud: Dynamic resource scaling, automatic replication, and point-in-time recovery are provided as standard when using the managed service.

10. Monitoring

Core monitoring queries

-- 1. Hypertable별 전체 크기 확인
SELECT
    hypertable_name,
    pg_size_pretty(hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)) AS total_size,
    pg_size_pretty(pg_total_relation_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)
        - pg_relation_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass)) AS index_size,
    num_chunks,
    compression_enabled
FROM timescaledb_information.hypertables
ORDER BY hypertable_size(format('%I.%I', hypertable_schema, hypertable_name)::regclass) DESC;

-- 2. 청크별 상세 정보 (크기, 압축 여부, 행 수)
SELECT
    chunk_name,
    range_start,
    range_end,
    is_compressed,
    pg_size_pretty(
        pg_total_relation_size(format('%I.%I', chunk_schema, chunk_name)::regclass)
    ) AS chunk_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_end DESC
LIMIT 20;

-- 3. 백그라운드 작업(Jobs) 상태 모니터링
SELECT
    job_id,
    application_name,
    proc_name,
    hypertable_name,
    schedule_interval,
    last_run_status,
    last_run_started_at,
    last_run_duration,
    next_start,
    total_successes,
    total_failures
FROM timescaledb_information.job_stats js
JOIN timescaledb_information.jobs j USING (job_id)
ORDER BY last_run_started_at DESC;

-- 4. 압축 효율 종합 리포트
SELECT
    hypertable_name,
    pg_size_pretty(SUM(before_compression_total_bytes)) AS before_compression,
    pg_size_pretty(SUM(after_compression_total_bytes))  AS after_compression,
    ROUND(
        (1 - SUM(after_compression_total_bytes)::numeric
             / NULLIF(SUM(before_compression_total_bytes), 0)) * 100, 1
    ) AS savings_pct,
    COUNT(*) AS compressed_chunks
FROM timescaledb_information.compressed_chunk_stats
GROUP BY hypertable_name;

Prometheus + Grafana integration

TimescaleDBpg_prometheusYou can also store Prometheus metrics directly through the adapter or Promscale (now deprecated, the successor is Timescale Cloud, which has absorbed the functionality of Promscale). Conversely, to collect metrics from TimescaleDB itself into Prometheus:postgres_exporterUse .

11. Time series database comparison

ItemTimescaleDBInfluxDB (3.x)ClickHouse
Base technologyPostgreSQL extensionsApache Arrow + DataFusionOwn Engine (C++)
query languageStandard SQL (fully compatible)SQL (replaces InfluxQL), Flight SQLSQL (own dialect)
JOIN supportFull support (PostgreSQL features)LIMITEDSupport (Beware of large JOINs)
Compression rate90~95% (column-oriented conversion)High (Parquet-based)90~95% (column-oriented native)
Continuous countingNative support (incremental updates)Task-based aggregationMaterialized View (INSERT trigger)
Data Retention PolicyNative (automatic chunk deletion)Bucket-based retentionTTL (partition/row unit)
Transaction SupportFully ACID (PostgreSQL)Not supportedNot supported
learning curveLow (if you are a PostgreSQL user)Medium (transiting to new architecture)Medium (many unique concepts)
EcosystemPostgreSQL full (pg extension, ORM)Telegraf, KapacitorIndependent ecosystem
Main use casesIoT, metrics, PostgreSQL extensionsDevOps Metrics, IoTLarge-scale analytics, logs
LicenseApache 2.0 (Community)MIT/Apache 2.0Apache 2.0

Selection Guide:

  • If you are already running PostgreSQL and SQL compatibility and transactions are important: TimescaleDB is the best option. There is no need to operate an additional separate database.
  • If a pure metrics/log collection pipeline is key and you want to take advantage of the Telegraf ecosystem: InfluxDB is the way to go.
  • If you need subsecond response for analytics workloads of tens of TB or more: ClickHouse is optimal. However, ACID transactions must be abandoned.

12. Troubleshooting

Issue 1: Poor INSERT performance

Symptom: INSERT delay time gradually increases. In particular, spikes occur at chunk boundaries (when new chunks are created).

Causes and Solutions:

  • If frequent chunk creation occurs because the chunk interval is too short: Increase the chunk interval.
  • If the index is excessive: Remove unnecessary indexes. In time series data, adding one index can degrade INSERT performance by 10-20%.
  • WAL write bottleneck:wal_buffers, max_wal_sizeincreases.

Issue 2: Slow queries on compressed chunks

Symptom: When searching compressed data, it is slower than uncompressed data.

Causes and Solutions:

-compress_segmentbydoes not match the query pattern: Queries that do not filter by segment key must release all segments. According to the query patternsegmentbyChange the settings (existing compressed data needs to be decompressed and recompressed).

  • for compressed chunksORDER BYgocompress_orderbyIf different from: Adjust the alignment direction.

Issue 3: Successive aggregate updates fail

Symptoms:timescaledb_information.job_statsof continuous aggregation operations inlast_run_statusgoFailedIt is displayed as .

Causes and Solutions:

-- 실패한 작업 상세 확인
SELECT
    job_id,
    proc_name,
    last_run_status,
    last_run_started_at,
    last_run_duration,
    config
FROM timescaledb_information.job_stats js
JOIN timescaledb_information.jobs j USING (job_id)
WHERE last_run_status = 'Failed';

-- 작업 로그에서 오류 메시지 확인 (PostgreSQL 로그)
-- 일반적인 원인: 메모리 부족(work_mem), 디스크 공간 부족, 락 경합

-- 수동으로 연속 집계 갱신 실행 (디버깅 목적)
CALL refresh_continuous_aggregate('sensor_hourly',
    now() - INTERVAL '3 hours',
    now() - INTERVAL '1 hour'
);

Problem 4: Disk space explosion

Symptom: Disk usage increases faster than expected, affecting services.

Solution Order:

  1. Check whether the compression policy is operating normally.
  2. Check whether the retention policy is set.
  3. Manually compress older uncompressed chunks.
  4. Immediately delete unnecessary old chunks.

13. Precautions during operation

DML limitations on compressed data

INSERT, UPDATE, and DELETE cannot be executed directly on compressed chunks. To process late-arriving data within the compressed time range, the corresponding chunk must first be released.

-- 특정 청크 압축 해제
SELECT decompress_chunk('_timescaledb_internal._hyper_1_3_chunk');

-- 데이터 INSERT/UPDATE 수행 후 다시 압축
SELECT compress_chunk('_timescaledb_internal._hyper_1_3_chunk');

If late-arriving data is frequent, the offset to exclude the most recent N days from compression should be set sufficiently large.

Precautions when migrating

  • When upgrading TimescaleDB major version (e.g. 2.x -> 3.x), be sure to follow the official migration guide.
  • PostgreSQL major version upgrade and TimescaleDB upgrade are not performed at the same time. Proceed one by one sequentially.
  • Before upgradepg_dumpPerform a schema backup and, if possible, verify it first in a test environment.

Connection Pooling

TimescaleDB uses background workers internally, somax_connectionsThere should be room for When using PgBouncertransactionUse mode and pay attention to prepared statement compatibility.

14. Failure cases and recovery

Case 1: Degraded performance due to unset chunk interval

One team loaded 100 GB of sensor data per day using the default chunk interval (7 days). The active chunk reached 700GB, exceeding 20 times the memory (32GB), and the INSERT delay increased to several seconds.

Recovery: Chunk interval was changed to 1 hour and existing data was migrated. Since changes to chunk intervals are applied starting from newly created chunks, existing data had to be moved to the new Hypertable.

-- 청크 간격 변경 (새 청크부터 적용)
SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 hour');

Case 2: Deterioration of query performance due to not setting compressed segmentby

With compression enabledcompress_segmentbywas not specified. All data is compressed into a single segment,WHERE sensor_id = 42The same filter query had to free an entire chunk. Queries became 3 times slower than before compression.

Recover: Release all compressed chunks,compress_segmentbyAfter setting it, it was recompressed. With large amounts of data, this process can take hours, so it's important to get it set up right the first time.

Case 3: Storage exhaustion due to operating without a retention policy

IoT sensor data was loaded without a retention policy for 3 years. The total data size reached 15TB, and most analysis queries used only the last 30 days of data. Storage costs were running into thousands of dollars per month.

Recovery: First obtained hourly/daily aggregation of past data by creating a continuous aggregation, and then applied a 90-day retention policy to the original data. Storage decreased from 15TB to 800GB.

15. Production Checklist

Hypertable design

  • Is the chunk interval set appropriately for the daily data volume and memory?
  • Have you considered whether multidimensional partitioning is necessary (if the number of unique devices/sensors is very high)?
  • Is the time zone processing method of the time column unified? (TIMESTAMPTZ recommended)

index

  • Is there a composite index that matches the main query pattern?
  • Are unnecessary indexes degrading INSERT performance?
  • Did you optimize the index size by using partial indexes?

Compression

  • [ ]compress_segmentbymatches the query pattern
  • Does the time offset of the compression policy take late-arriving data into account?
  • Is the compression ratio achieving the expected level (80% or more)?

Continuous counting

  • Is continuous aggregation utilized in dashboard/report queries?
  • Are the update cycle and offset set according to the data delay characteristics?
  • Is hierarchical aggregation (hourly -> daily) configured?

Data retention

  • Has the retention period for the original data been set?
  • Is the retention period of aggregated data set longer than the original?
  • Is the retention policy job running normally (check job_stats)?

Monitoring

  • Are disk usage monitoring and notifications set up?
  • Is the execution status of background tasks (compression, preservation, aggregation) monitored?
  • Is slow query logging enabled (log_min_duration_statement)
  • Isn’t the number of chunks increasing excessively?

Backup and recovery

  • [ ]pg_dumporpg_basebackupHave regular backups been set up?
  • Was recovery testing performed regularly?
  • Is point-in-time recovery possible with WAL archiving?

16. References

Quiz

Q1: What is the main topic covered in "TimescaleDB Time Series Database Operation Guide: From Hypertable Design to Continuous Aggregation and Compression Optimization"?

A comprehensive operations guide that covers TimescaleDB's Hypertable architecture and chunk management, Continuous Aggregates settings, 90% storage reduction with native compression, data retention policy, and production query optimization and troubleshooting.

Q2: Describe the TimescaleDB Architecture: Hypertable and Chunk. Structure of Hypertable The core of TimescaleDB is Hypertable. It appears to be a single table to the user, but internally it is composed of multiple chunks that are automatically divided based on the time axis.

Q3: Explain the core concept of Data Retention Policy. Time series data often loses value over time. A hierarchical preservation strategy that deletes old original data but preserves aggregated data longer is effective.```sql -- 90일 이상 된 원본 데이터 자동 삭제 SELECT add_retention_policy('sensor_data', INTERVAL '90 days'); -- 시간별 집계는 1년 보존 SEL...

Q4: What are the key aspects of Time series database comparison? Selection Guide: If you are already running PostgreSQL and SQL compatibility and transactions are important: TimescaleDB is the best option. There is no need to operate an additional separate database.

Q5: What approach is recommended for Troubleshooting? Issue 1: Poor INSERT performance Symptom: INSERT delay time gradually increases. In particular, spikes occur at chunk boundaries (when new chunks are created).