☀️Siang
Dashboard & Cloud

TimescaleDB untuk Time Series IoT

Tutorial lengkap TimescaleDB untuk data time series IoT. Pelajari hypertables, continuous aggregates, compression, dan retention policies untuk data sensor yang efisien.

Artikel: Timescaledb Iot Artikel: Timescaledb Iot


1. Apa Itu TimescaleDB?

TimescaleDB adalah extension PostgreSQL yang dioptimalkan untuk time-series data. Berbeda dengan database time-series khusus (seperti InfluxDB), TimescaleDB adalah full PostgreSQL — artinya kamu mendapatkan semua fitur PostgreSQL (SQL, JOIN, ACID, indexes) plus optimasi khusus untuk data berbasis waktu.

Keunggulan TimescaleDB adalah kemampuan untuk menyimpan data time-series dan relational dalam satu database. Kamu bisa JOIN data sensor dengan data device, data lokasi, dan data user tanpa perlu ETL ke database berbeda.

💡 Tips

TimescaleDB open-source (Apache 2.0 license) untuk fitur dasar. Fitur advanced seperti compression dan continuous aggregates tersedia di Community Edition. Multi-node dan cloud features ada di Timescale Cloud (managed service).

TimescaleDB vs Alternatif

AspekTimescaleDBInfluxDBPostgreSQL Plain
BasisPostgreSQL extensionDatabase khususDatabase umum
SQL SupportFull PostgreSQL SQLInfluxQL/FluxFull SQL
Time-SeriesDioptimalkan (hypertable)DioptimalkanTidak (manual partitioning)
JOINYa (relational + time series)TidakYa
CompressionYa (90-95% ratio)YaTidak built-in
Continuous AggYa (auto-refresh)TasksManual (materialized view)
RetentionYa (drop chunks)YaManual

2. Instalasi & Setup

Docker Compose

# docker-compose.yml
version: "3.8"
services:
  timescaledb:
    image: timescale/timescaledb:latest-pg16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: tsdb123
      POSTGRES_DB: iot_data
    volumes:
      - tsdb-data:/var/lib/postgresql/data
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.0.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
    volumes:
      - grafana-data:/var/lib/grafana
    restart: unless-stopped

volumes:
  tsdb-data:
  grafana-data:

Inisialisasi Database

-- Enable TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- Buat tabel untuk sensor data
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    location    TEXT,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    pressure    DOUBLE PRECISION,
    battery     INTEGER
);

-- Buat tabel untuk device info
CREATE TABLE devices (
    device_id   TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    type        TEXT,
    location    TEXT,
    firmware    TEXT,
    installed   DATE
);

-- Buat tabel untuk alerts
CREATE TABLE alerts (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    type        TEXT NOT NULL,
    value       DOUBLE PRECISION,
    threshold   DOUBLE PRECISION,
    status      TEXT DEFAULT 'active'
);

-- Convert ke hypertable (time-series optimization)
SELECT create_hypertable('sensor_data', 'time');
SELECT create_hypertable('alerts', 'time');

-- Buat indexes
CREATE INDEX ON sensor_data (device_id, time DESC);
CREATE INDEX ON sensor_data (location, time DESC);
CREATE INDEX ON alerts (device_id, time DESC);
CREATE INDEX ON alerts (status, time DESC);

3. Hypertables

Hypertable adalah abstraksi TimescaleDB di atas PostgreSQL native partitioning. Ketika kamu membuat hypertable, TimescaleDB otomatis mempartisi data berdasarkan waktu (time dimension) dan opsi-opsi space dimension (misalnya device_id).

Cara Kerja Hypertable

# Hypertable mempartisi data ke "chunks":
# - Setiap chunk berisi data untuk rentang waktu tertentu
# - Default chunk interval: 7 hari (auto-tuned)
# - Chunk dibuat otomatis saat data baru masuk
# - Query hanya mengakses chunk yang relevan (chunk exclusion)

# Visualisasi:
# sensor_data (hypertable)
#   ├── _hyper_1_chunk_1  (2026-01-01 to 2026-01-08)
#   ├── _hyper_1_chunk_2  (2026-01-08 to 2026-01-15)
#   ├── _hyper_1_chunk_3  (2026-01-15 to 2026-01-22)
#   └── ...

# Chunk sizing guidelines:
# - Data volume < 1M rows/day → chunk: 1 week
# - Data volume 1M-10M rows/day → chunk: 1 day
# - Data volume > 10M rows/day → chunk: 1 hour

Multi-dimensional Partitioning

-- Hypertable dengan space partitioning berdasarkan device_id
-- Membagi setiap chunk berdasarkan device_id hash
SELECT create_hypertable(
    'sensor_data',
    'time',
    'device_id',
    number_partitions => 4,
    chunk_time_interval => INTERVAL '1 day'
);

-- Monitor chunks
SELECT * FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start DESC;

-- Adjust chunk interval
SELECT set_chunk_time_interval('sensor_data', INTERVAL '12 hours');

4. Continuous Aggregates

Continuous aggregates adalah materialized view yang otomatis di-refresh saat data baru masuk. Ini sangat berguna untuk downsampling data IoT — misalnya membuat data per-menit dari data per-detik.

-- Continuous aggregate: data per 5 menit
CREATE MATERIALIZED VIEW sensor_5min
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('5 minutes', time) AS bucket,
    device_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,
    STDDEV(temperature) AS temp_stddev
FROM sensor_data
GROUP BY bucket, device_id, location
WITH NO DATA;

-- Refresh policy: update setiap 5 menit
SELECT add_continuous_aggregate_policy('sensor_5min',
    start_offset => INTERVAL '1 hour',
    end_offset => INTERVAL '5 minutes',
    schedule_interval => INTERVAL '5 minutes'
);

-- Continuous aggregate: data per jam
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    MIN(temperature) AS min_temp,
    MAX(temperature) AS max_temp,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY temperature) AS p95_temp,
    AVG(humidity) AS avg_humidity,
    COUNT(*) AS readings,
    MAX(battery) AS latest_battery
FROM sensor_data
GROUP BY bucket, device_id
WITH NO DATA;

-- Continuous aggregate: data per hari
CREATE MATERIALIZED VIEW sensor_daily
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 day', time) AS bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    MIN(temperature) AS min_temp,
    MAX(temperature) AS max_temp,
    AVG(humidity) AS avg_humidity,
    SUM(CASE WHEN temperature > 35 THEN 1 ELSE 0 END) AS high_temp_count,
    COUNT(*) AS total_readings
FROM sensor_data
GROUP BY bucket, device_id
WITH NO DATA;

-- Query continuous aggregate (super cepat!)
SELECT * FROM sensor_hourly
WHERE device_id = 'sensor-01'
  AND bucket > NOW() - INTERVAL '7 days'
ORDER BY bucket DESC;

5. Compression

TimescaleDB memiliki fitur native compression yang bisa mengurangi ukuran data 90-95% untuk time-series data. Compression bekerja per-chunk — data diubah dari row-based ke columnar format.

-- Enable compression pada hypertable
ALTER TABLE sensor_data SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device_id',
    timescaledb.compress_orderby = 'time DESC'
);

-- Compression policy: compress chunk yang lebih dari 7 hari
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');

-- Cek status compression
SELECT * FROM timescaledb_information.compressed_chunk_stats;

-- Manual compress chunk spesifik
SELECT compress_chunk('_hyper_1_chunk_5');

-- Manual decompress (untuk update data lama)
SELECT decompress_chunk('_hyper_1_chunk_5');

-- Cek compression ratio
SELECT
    hypertable_name,
    compression_status,
    before_compression_total_bytes,
    after_compression_total_bytes,
    pg_size_pretty(before_compression_total_bytes) AS before_size,
    pg_size_pretty(after_compression_total_bytes) AS after_size
FROM timescaledb_information.compressed_chunk_stats;

6. Retention Policies

Retention policy menghapus data lama secara otomatis dengan me-drop chunks yang sudah expired. Ini sangat efisien karena dropping entire chunk jauh lebih cepat dari DELETE rows.

-- Retention policy: hapus data raw setelah 30 hari
SELECT add_retention_policy('sensor_data', INTERVAL '30 days');

-- Retention policy untuk alerts: simpan 90 hari
SELECT add_retention_policy('alerts', INTERVAL '90 days');

-- Manual drop chunk
SELECT drop_chunks('sensor_data', INTERVAL '60 days');

-- Cek kapan chunk akan dihapus
SELECT
    hypertable_name,
    chunk_name,
    range_start,
    range_end,
    pg_size_pretty(total_bytes) as size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;

-- Disable retention policy
SELECT remove_retention_policy('sensor_data');

-- Re-enable
SELECT add_retention_policy('sensor_data', INTERVAL '30 days');

7. Optimized Queries untuk IoT

-- 1. Ambil data terbaru dari semua device
SELECT DISTINCT ON (device_id)
    device_id, time, temperature, humidity, battery
FROM sensor_data
ORDER BY device_id, time DESC;

-- 2. Time bucket query (5 menit rata-rata)
SELECT
    time_bucket('5 minutes', time) AS bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    AVG(humidity) AS avg_humidity
FROM sensor_data
WHERE time > NOW() - INTERVAL '1 hour'
  AND device_id = 'sensor-01'
GROUP BY bucket, device_id
ORDER BY bucket DESC;

-- 3. Moving average (30 menit window)
SELECT
    time,
    device_id,
    temperature,
    AVG(temperature) OVER (
        PARTITION BY device_id
        ORDER BY time
        RANGE BETWEEN INTERVAL '30 minutes' PRECEDING AND CURRENT ROW
    ) AS moving_avg_30m
FROM sensor_data
WHERE device_id = 'sensor-01'
  AND time > NOW() - INTERVAL '24 hours';

-- 4. Anomaly detection (z-score)
WITH stats AS (
    SELECT device_id,
           AVG(temperature) AS mean_temp,
           STDDEV(temperature) AS std_temp
    FROM sensor_data
    WHERE time > NOW() - INTERVAL '7 days'
    GROUP BY device_id
)
SELECT s.time, s.device_id, s.temperature,
       (s.temperature - st.mean_temp) / NULLIF(st.std_temp, 0) AS z_score
FROM sensor_data s
JOIN stats st ON s.device_id = st.device_id
WHERE s.time > NOW() - INTERVAL '1 hour'
  AND ABS((s.temperature - st.mean_temp) / NULLIF(st.std_temp, 0)) > 3
ORDER BY s.time DESC;

-- 5. Latest reading per device dengan LATERAL join
SELECT d.device_id, d.name, d.location, s.time, s.temperature, s.battery
FROM devices d
CROSS JOIN LATERAL (
    SELECT time, temperature, battery
    FROM sensor_data
    WHERE device_id = d.device_id
    ORDER BY time DESC
    LIMIT 1
) s;

8. Integrasi dengan Grafana

# Menambahkan TimescaleDB ke Grafana:
# 1. Grafana → Configuration → Data Sources → Add data source
# 2. Pilih "PostgreSQL"
# 3. Konfigurasi:
#    Host: timescaledb:5432
#    Database: iot_data
#    User: postgres
#    Password: tsdb123
#    SSL Mode: disable (untuk internal network)
# 4. Enable "TimescaleDB" toggle
# 5. Save & Test

# Contoh Grafana query:
# Panel: Temperature time series
SELECT
  $__time(bucket),
  device_id AS metric,
  avg_temp AS "Avg Temperature"
FROM sensor_5min
WHERE $__timeFilter(bucket)
  AND device_id IN ($device_ids)
ORDER BY bucket

# Panel: Device health table
SELECT
  d.device_id AS "Device",
  s.time AS "Last Seen",
  s.temperature AS "Temp (°C)",
  s.battery AS "Battery (%)",
  CASE
    WHEN s.time > NOW() - INTERVAL '5 minutes' THEN '🟢 Online'
    WHEN s.time > NOW() - INTERVAL '1 hour' THEN '🟡 Stale'
    ELSE '🔴 Offline'
  END AS "Status"
FROM devices d
CROSS JOIN LATERAL (
  SELECT time, temperature, battery
  FROM sensor_data WHERE device_id = d.device_id
  ORDER BY time DESC LIMIT 1
) s

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang TimescaleDB:

Pertanyaan 1: Apa keunggulan utama TimescaleDB dibanding InfluxDB?

a) Lebih cepat
b) Full PostgreSQL — bisa JOIN data time-series dengan data relational
c) Gratis selamanya
d) Tidak perlu instalasi

Pertanyaan 2: Apa itu hypertable?

a) Tabel PostgreSQL yang sangat besar
b) Abstraksi yang otomatis mempartisi data berdasarkan waktu ke chunks
c) Tabel yang hanya bisa menyimpan angka
d) Tabel yang di-replicate ke banyak server

Pertanyaan 3: Apa fungsi continuous aggregates?

a) Menghapus data lama otomatis
b) Materialized view yang otomatis di-refresh untuk downsampling data
c) Mengkompresi data
d) Mengenkripsi data

Pertanyaan 4: Mengapa compression di TimescaleDB sangat efisien untuk time-series?

a) Karena data dihapus setelah dikompresi
b) Karena data berubah dari row-based ke columnar format, dan data time-series memiliki pola yang sangat bisa dikompresi
c) Karena data dikirim ke cloud
d) Karena semua kolom menjadi NULL

Pertanyaan 5: Mengapa retention policy di TimescaleDB sangat cepat dibanding DELETE?

a) Karena menggunakan SSD
b) Karena me-drop entire chunk (partition) langsung, bukan menghapus baris per baris
c) Karena data diarsipkan ke S3
d) Karena data tidak benar-benar dihapus
 ad-slot-wide"> 
← SebelumnyaNode-RED Advanced Selanjutnya →EMQX MQTT Broker
🔍 Zoom
100%
🎨 Tema