Skip to main content

TimescaleDB

Iotistica uses TimescaleDB on top of PostgreSQL 16 for all time-series storage. TimescaleDB provides automatic chunk partitioning, native compression, continuous aggregates, and data retention policies — all managed transparently by the database.

All hypertables, compression settings, retention policies, and continuous aggregates are applied by the database migrations under api/database/migrations/.


Hypertables​

Five tables are converted to TimescaleDB hypertables. Each is partitioned by time into fixed-size chunks that are compressed and eventually dropped automatically.

HypertableTime columnChunk intervalPurpose
readingstime1 dayNormalized sensor telemetry — the primary time-series table
agent_metricsrecorded_at7 daysAgent system health: CPU, memory, storage, temperature
agent_logstimestamp1 dayAgent and service log events
mqtt_broker_statstimestamp1 dayBroker-level statistics: connected clients, message throughput
mqtt_topic_metricstimestamp1 dayPer-topic message counts, QoS distribution, byte rates

readings Schema​

The primary hypertable. Every sensor reading from every protocol (Modbus, OPC-UA, BACnet, MQTT, SNMP, CAN) lands here as a normalized row.

CREATE TABLE readings (
time timestamptz NOT NULL,
agent_uuid uuid NOT NULL,
metric_name text NOT NULL, -- e.g. "plc-1.temperature"
value double precision,
quality text DEFAULT 'good', -- good | bad | uncertain
unit text, -- °C, %, V, hPa, …
protocol text NOT NULL, -- modbus | opcua | mqtt | …
extra jsonb DEFAULT '{}', -- deviceName, slave_id, nodeId, …
anomaly_score double precision, -- 0–1, NULL if not computed
anomaly_threshold double precision,
PRIMARY KEY (agent_uuid, metric_name, time)
);

Key indexes:

  • (agent_uuid, time DESC) — device + time range queries
  • (metric_name, time DESC) — cross-agent metric queries
  • (protocol, time DESC) — filter by protocol
  • GIN (extra) — JSONB metadata queries

Compression Policies​

All hypertables use LZ4-based column compression. Compression is applied automatically to chunks older than the threshold. Typical storage reduction is 85–95% for sensor data.

HypertableCompress afterSegment byOrder by
readings7 daysagent_uuid, metric_nametime DESC
agent_metrics7 daysagent_uuidrecorded_at DESC
agent_logs7 daysagent_uuid, service_nametimestamp DESC
mqtt_broker_stats7 days—timestamp DESC
mqtt_topic_metrics7 daystopictimestamp DESC

compress_segmentby groups related rows together within each chunk before compression, improving both compression ratio and query performance on the segmented columns. Queries on compressed chunks are served without explicit decompression — TimescaleDB decompresses on the fly at the chunk level.


Retention Policies​

Data older than the retention threshold is automatically deleted by a background TimescaleDB job. The job runs once per day.

HypertableRetention
readings730 days (2 years)
agent_metrics90 days
agent_logs90 days
mqtt_broker_stats180 days
mqtt_topic_metrics180 days
note

Retention applies to raw hypertable chunks. Continuous aggregates have independent refresh and retention policies and are not deleted when raw data is dropped.


Continuous Aggregates​

TimescaleDB continuous aggregates are materialized views that are refreshed incrementally in the background. They pre-aggregate raw data into coarser time buckets so dashboard queries hit the aggregate instead of scanning raw chunks.

readings aggregates​

ViewBucketRefresh scheduleStart offsetEnd offsetColumns
readings_1m1 minuteevery 1 min1 hour1 minuteavg/min/max value, sample_count, quality_ratio, anomaly metrics
readings_1h1 hourevery 1 hour1 day1 houravg/min/max/stddev value, sample_count, quality_ratio
readings_hourly1 hourevery 1 hour3 hours1 houravg/min/max/stddev, first/last value + timestamp
readings_daily1 dayevery 1 day3 days1 dayavg/min/max/stddev value, sample_count

readings_1m and readings_1h materialize extra->>'deviceName' and protocol for per-device, per-protocol breakdown. readings_hourly and readings_daily aggregate at the (agent_uuid, metric_name, protocol) grain without device name (used by fleet-level dashboards).

agent_metrics aggregates​

ViewBucketRefresh scheduleColumns
agent_metrics_5min5 minutesevery 5 minavg/max/min cpu_usage, cpu_temp, memory/storage usage
agent_metrics_hourly1 hourevery 1 hoursame
agent_metrics_daily1 dayevery 1 daysame

agent_logs aggregates​

ViewBucketRefresh scheduleColumns
device_logs_5min5 minutesevery 5 minper-service error/warn/info/debug counts, error_samples
device_logs_hourly1 hourevery 1 hoursame

mqtt_broker_stats aggregates​

ViewBucketRefresh scheduleColumns
mqtt_broker_stats_5min5 minutesevery 5 minavg/max connected_clients, message rates, byte totals
mqtt_broker_stats_hourly1 hourevery 1 hoursame
mqtt_broker_stats_daily1 dayevery 1 daysame

mqtt_topic_metrics aggregates​

ViewBucketRefresh scheduleColumns
mqtt_topic_metrics_5min5 minutesevery 5 minper-topic message counts, byte totals, QoS distribution
mqtt_topic_metrics_hourly1 hourevery 1 hoursame
mqtt_topic_metrics_daily1 dayevery 1 daysame

Operational Queries​

Check hypertables:

SELECT hypertable_name, num_chunks, compression_enabled
FROM timescaledb_information.hypertables;

Check compression and retention jobs:

SELECT hypertable_name, proc_name, schedule_interval, next_start, last_run_status
FROM timescaledb_information.jobs
WHERE proc_name IN ('policy_compression', 'policy_retention')
ORDER BY hypertable_name;

Check continuous aggregate refresh policies:

SELECT view_name, schedule_interval, next_start, last_run_status
FROM timescaledb_information.jobs j
JOIN timescaledb_information.continuous_aggregates ca
ON j.hypertable_id = ca.mat_hypertable_id
ORDER BY view_name;

Check storage savings:

SELECT
hypertable_name,
pg_size_pretty(before_compression_total_bytes) AS before,
pg_size_pretty(after_compression_total_bytes) AS after,
ROUND(
(1 - after_compression_total_bytes::numeric / NULLIF(before_compression_total_bytes, 0)) * 100, 1
) AS pct_saved
FROM chunk_compression_stats('readings')
ORDER BY chunk_name;

Ingest stream depth (consumer lag check):

SELECT chunk_name, range_start, range_end, is_compressed, num_rows
FROM timescaledb_information.chunks
WHERE hypertable_name = 'readings'
ORDER BY range_end DESC
LIMIT 10;