TimescaleDB로 1분마다 쌓이는 센서 로그 관리하기 - PostgreSQL 시계열 데이터 실전 가이드

learning by Seven Fingers Studio 15분
TimescaleDBPostgreSQL시계열데이터데이터베이스백엔드

센서 로그, IoT 데이터, 모니터링 지표… 시간 기반 데이터를 다루다 보면 테이블이 순식간에 수천만 건이 됩니다. 저도 최근에 8개 장비에서 1분마다 로그가 들어오는 시스템을 구축했는데, 일반 PostgreSQL로는 한계가 있더라고요.

그래서 찾은 게 TimescaleDB입니다. PostgreSQL 확장이라 기존 인프라 그대로 쓸 수 있고, 시계열 데이터에 최적화되어 있어요.

TimescaleDB가 뭔가요?

TimescaleDB는 PostgreSQL 위에서 동작하는 시계열 데이터베이스 확장입니다. 일반 테이블처럼 보이지만 내부적으로는 시간 기준으로 데이터를 자동 분할(파티셔닝)해서 저장해요.

왜 필요한가?

  • 1분마다 8개 장비 로그 = 하루 11,520건
  • 1년이면 약 420만 건
  • 일반 테이블로는 조회 성능이 급격히 떨어짐

TimescaleDB의 하이퍼테이블(Hypertable)을 쓰면 이 문제가 해결됩니다.

설치하기

Docker로 가장 쉽게 시작할 수 있어요.

docker run -d --name timescaledb \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=password \
  timescale/timescaledb:latest-pg16

기존 PostgreSQL에 확장으로 설치하려면:

-- 확장 설치
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- 설치 확인
\dx timescaledb

테이블 설계 - 8개 장비 로그 구조

제 경우 8개 장비에서 1분마다 상태 로그가 들어옵니다. 테이블 구조는 이렇게 잡았어요.

CREATE TABLE device_logs (
    time        TIMESTAMPTZ NOT NULL,
    device_id   INTEGER NOT NULL,
    status      VARCHAR(20),
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    error_code  INTEGER DEFAULT 0,
    raw_data    JSONB
);

컬럼 설명:

  • time: 로그 수집 시각 (필수, 시계열 기준)
  • device_id: 장비 번호 (1~8)
  • status: 상태값 (running, idle, error 등)
  • temperature, humidity: 센서 수치
  • error_code: 에러 코드 (0이면 정상)
  • raw_data: API에서 받은 원본 JSON

하이퍼테이블로 변환

일반 테이블을 하이퍼테이블로 바꾸는 건 한 줄이면 됩니다.

SELECT create_hypertable('device_logs', 'time');

이렇게 하면 TimescaleDB가 자동으로 시간 기준 청크(chunk)를 만들어서 데이터를 분산 저장해요. 기본값은 7일 단위입니다.

청크 간격 변경하기:

1분마다 데이터가 들어오니까 저는 1일 단위로 줄였어요.

SELECT create_hypertable(
    'device_logs',
    'time',
    chunk_time_interval => INTERVAL '1 day'
);

인덱스 설정

시계열 데이터는 보통 “특정 장비의 최근 데이터”를 조회하는 경우가 많아요. 복합 인덱스를 걸어두면 좋습니다.

-- 장비별 + 시간순 조회용
CREATE INDEX idx_device_time ON device_logs (device_id, time DESC);

-- 에러 로그만 빠르게 찾기
CREATE INDEX idx_error_logs ON device_logs (time DESC)
WHERE error_code != 0;

데이터 삽입

API에서 받은 로그를 저장하는 코드입니다.

INSERT INTO device_logs (time, device_id, status, temperature, humidity, error_code, raw_data)
VALUES (
    NOW(),
    1,
    'running',
    25.5,
    60.2,
    0,
    '{"voltage": 220, "current": 1.5}'::jsonb
);

배치 삽입 (8개 장비 한번에):

INSERT INTO device_logs (time, device_id, status, temperature, humidity)
VALUES
    (NOW(), 1, 'running', 25.5, 60.2),
    (NOW(), 2, 'running', 26.1, 58.5),
    (NOW(), 3, 'idle', 24.8, 62.0),
    (NOW(), 4, 'running', 25.9, 59.8),
    (NOW(), 5, 'error', 30.2, 55.0),
    (NOW(), 6, 'running', 25.3, 61.5),
    (NOW(), 7, 'running', 26.0, 60.0),
    (NOW(), 8, 'idle', 24.5, 63.2);

시계열 쿼리 활용하기

TimescaleDB의 진가는 시계열 전용 함수들입니다.

최근 1시간 데이터 조회

SELECT * FROM device_logs
WHERE time > NOW() - INTERVAL '1 hour'
ORDER BY time DESC;

장비별 최신 상태 확인

SELECT DISTINCT ON (device_id)
    device_id, time, status, temperature
FROM device_logs
ORDER BY device_id, time DESC;

시간대별 평균값 집계 (time_bucket)

이게 TimescaleDB의 핵심 기능이에요. time_bucket 함수로 시간을 묶어서 집계할 수 있습니다.

-- 1시간 단위 평균 온도
SELECT
    time_bucket('1 hour', time) AS hour,
    device_id,
    AVG(temperature) AS avg_temp,
    MAX(temperature) AS max_temp,
    MIN(temperature) AS min_temp
FROM device_logs
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY hour, device_id
ORDER BY hour DESC, device_id;

일별 에러 발생 횟수

SELECT
    time_bucket('1 day', time) AS day,
    device_id,
    COUNT(*) FILTER (WHERE error_code != 0) AS error_count
FROM device_logs
WHERE time > NOW() - INTERVAL '7 days'
GROUP BY day, device_id
ORDER BY day DESC;

연속 집계 (Continuous Aggregates)

매번 집계 쿼리 돌리면 느립니다. TimescaleDB는 연속 집계 기능으로 미리 계산해둘 수 있어요.

-- 1시간 단위 집계 뷰 생성
CREATE MATERIALIZED VIEW device_hourly_stats
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS hour,
    device_id,
    AVG(temperature) AS avg_temp,
    AVG(humidity) AS avg_humidity,
    COUNT(*) AS log_count,
    COUNT(*) FILTER (WHERE error_code != 0) AS error_count
FROM device_logs
GROUP BY hour, device_id;

-- 자동 갱신 정책 설정 (1시간마다)
SELECT add_continuous_aggregate_policy('device_hourly_stats',
    start_offset => INTERVAL '3 hours',
    end_offset => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour'
);

이제 집계 조회가 훨씬 빨라집니다.

SELECT * FROM device_hourly_stats
WHERE hour > NOW() - INTERVAL '7 days'
ORDER BY hour DESC;

데이터 보관 정책 (Retention)

시계열 데이터는 오래된 건 지워야 해요. 자동 삭제 정책을 걸 수 있습니다.

-- 90일 지난 데이터 자동 삭제
SELECT add_retention_policy('device_logs', INTERVAL '90 days');

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

압축으로 용량 절약

오래된 데이터는 압축해서 저장 공간을 아낄 수 있어요.

-- 압축 활성화
ALTER TABLE device_logs SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device_id'
);

-- 7일 지난 청크 자동 압축
SELECT add_compression_policy('device_logs', INTERVAL '7 days');

압축률은 보통 90% 이상입니다. 1GB 데이터가 100MB 이하로 줄어들어요.

실제 운영 팁

1. 청크 크기 확인

SELECT chunk_name, range_start, range_end
FROM timescaledb_information.chunks
WHERE hypertable_name = 'device_logs';

2. 테이블 사이즈 확인

SELECT hypertable_size('device_logs');

3. 압축 상태 확인

SELECT * FROM timescaledb_information.compressed_chunk_stats;

시계열 데이터 관리, 생각보다 복잡하지 않죠? TimescaleDB 쓰면 PostgreSQL 그대로 쓰면서 시계열 최적화를 공짜로 얻을 수 있어요. 저처럼 센서 로그나 모니터링 데이터 다루는 분들께 강력 추천합니다!

← 블로그 목록으로