Introduction
TimescaleDB turns PostgreSQL into a time-series database optimized for billions of IoT, monitoring, or financial data points without sacrificing standard SQL performance. Unlike InfluxDB or Prometheus, it retains full PostgreSQL compatibility: complex joins, ACID transactions, and extensions like PostGIS. In 2026, with the rise of edge computing, mastering hypertables (automatically time-partitioned tables), compression (up to 90% savings), continuous aggregates (auto-refreshed materialized views), and hyperfunctions (vectorized UDF queries) is essential for data architects.
This expert tutorial guides you step by step: from Docker installation to production optimization, with copy-paste SQL code. Imagine storing 1 TB of metrics per day compressed to 100 GB, queried in <100ms. By the end, you'll bookmark this guide for your critical projects.
Prerequisites
- PostgreSQL 14+ (or Docker for simplicity)
- psql client installed
- Advanced SQL and PostgreSQL partitioning knowledge
- Minimum 4 GB RAM for testing (16+ GB recommended)
- Tools: Docker and optional pgAdmin for visualization
Installation with Docker Compose
version: '3.8'
services:
timescale:
image: timescale/timescaledb:latest-pg16
container_name: timescaledb
restart: always
environment:
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_USER: postgres
POSTGRES_DB: timescaledb_tutorial
ports:
- "5432:5432"
volumes:
- timescale_data:/var/lib/postgresql/data
volumes:
timescale_data:
driver: localThis docker-compose.yml deploys TimescaleDB 2.15+ on PostgreSQL 16 with persistent storage via volume. Run with docker compose up -d. The official image includes the extension enabled by default, avoiding manual installation pitfalls like shared_preload_libraries dependencies. Check with docker logs timescaledb.
Connecting and Enabling the Extension
Connect via psql -h localhost -U postgres -d timescaledb_tutorial (password: mysecretpassword). The timescaledb extension is pre-loaded, but enable it per database with CREATE EXTENSION. Test with SELECT extname FROM pg_extension WHERE extname = 'timescaledb';.
Creating a Basic Hypertable
CREATE TABLE metrics (
time TIMESTAMPTZ NOT NULL,
device_id TEXT NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
location POINT
);
SELECT create_hypertable('metrics', 'time', chunk_time_interval => INTERVAL '1 day', partitioning_column => 'device_id', number_partitions => 4);
-- Insert test data
INSERT INTO metrics (time, device_id, temperature, humidity, location)
SELECT
NOW() - INTERVAL '1 day' * generate_series(0, 99),
'device_' || (random() * 10)::INT,
20 + random() * 10,
40 + random() * 20,
POINT(random() * 100, random() * 100)
FROM generate_series(1, 10000);This hypertable partitions by time (daily chunks) and device_id (4 hash partitions). The 10k inserts simulate IoT sensors. Pitfall: without chunk_time_interval, the default 7 days is too large for high-velocity data; adjust to 1h for <1M rows/chunk.
Optimized Queries on Hypertables
Hypertables automatically apply constraint exclusion: only relevant chunks are scanned. Use time_bucket for bucketing and last() for latest values, just like in Grafana.
Advanced Queries with time_bucket
-- Hourly average per device
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
AVG(temperature) AS avg_temp,
LAST(humidity, time) AS last_humidity,
COUNT(*) AS points
FROM metrics
WHERE time > NOW() - INTERVAL '2 days'
GROUP BY 1, 2
ORDER BY 1 DESC, 2;
-- Top 5 hottest devices
SELECT
device_id,
MAX(temperature) AS max_temp,
ST_X(location) AS lon,
ST_Y(location) AS lat
FROM metrics
WHERE time > NOW() - INTERVAL '1 day'
GROUP BY device_id, location
ORDER BY max_temp DESC
LIMIT 5;time_bucket downsamples into buckets; LAST is more efficient than window functions. PostGIS POINT enables geo-queries. Result: <10ms on 1M rows thanks to chunk-level indexes. Avoid NOW() in production; use parameters for the query planner.
Continuous Aggregates
-- View for hourly aggregates
CREATE MATERIALIZED VIEW hourly_metrics
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
AVG(temperature) AS avg_temp,
AVG(humidity) AS avg_hum,
MIN(temperature) AS min_temp,
MAX(temperature) AS max_temp
FROM metrics
GROUP BY 1, 2
WITH DATA;
-- Refresh policies
SELECT add_continuous_aggregate_policy('hourly_metrics',
start_offset => INTERVAL '2 days',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour');
-- Query the view (always fresh)
SELECT * FROM hourly_metrics WHERE bucket > NOW() - INTERVAL '1 day';Continuous aggregates auto-refresh materialized views, perfect for dashboards. add_continuous_aggregate_policy schedules background jobs. Benefit: 100x faster queries on historical data. Pitfall: end_offset must match the bucket to avoid real-time lag.
Chunk Compression
Enable compression to reduce storage by 90% using Gorilla + Delta-of-Delta. Only chunks older than 7 days are eligible by default.
Enabling and Manual Compression
-- Enable compression on hypertable
ALTER TABLE metrics SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby = 'time DESC'
);
-- Compress the oldest chunk
SELECT compress_chunk(i.chunk_schema || '.' || i.chunk_name)
FROM timescaledb_information.chunks i
WHERE i.hypertable_name = 'metrics'
ORDER BY i.range_start DESC
LIMIT 1;
-- Decompress for writes (if needed)
SELECT decompress_chunk(c)
FROM show_chunks('metrics', older_than => INTERVAL '3 days') c;
-- Compression stats
SELECT
hypertable_name,
sg.num_rows_pre_compression,
sg.num_rows_post_compression,
pg_size_pretty(sg.size_pre_compression) AS size_pre,
pg_size_pretty(sg.size_post_compression) AS size_post
FROM timescaledb_information.compressed_chunk_stats sg
JOIN timescaledb_information.hypertables h ON h.hypertable_name = sg.hypertable_name
WHERE h.hypertable_name = 'metrics';compress_orderby optimizes partial decompression. segmentby groups by device for locality. Result: 10x size reduction. Caution: compression is read-only without decompression; test on staging.
Retention Policies
-- Auto-drop chunks older than 30 days
SELECT add_retention_policy('metrics',
INTERVAL '30 days');
-- Custom: drop only compressed
SELECT add_retention_policy('metrics',
INTERVAL '90 days',
if_exists => 'ignore');
-- List jobs
SELECT job_id, proc_name, last_run_status
FROM timescaledb_information.jobs
WHERE proc_name LIKE '%retention%';
-- Pause job
SELECT alter_job(job_id, scheduled => false)
FROM timescaledb_information.jobs
WHERE hypertable_name = 'metrics';Retention policies are background jobs that drop entire chunks, automatically freeing disk space. if_exists => 'ignore' avoids errors. Ideal for GDPR compliance. Pitfall: on compressed hypertables, space is retained until post-drop vacuum.
Advanced Hyperfunctions
In 2026, hyperfunctions like lttb() (Largest Triangle Three Buckets) downsample data for Grafana without visual loss.
LTTB Hyperfunction Example
-- Downsampling for graphs (max 100 points)
SELECT
time_bucket('5 minutes', time) AS bucket,
device_id,
lttb(temperature, 20, 'time') OVER (PARTITION BY device_id ORDER BY time)
FROM metrics
WHERE time > NOW() - INTERVAL '1 hour'
GROUP BY 1, 2
ORDER BY 1, 2;
-- Gap filling
SELECT
time_bucket('1 hour', generate_series(NOW() - INTERVAL '24 hours', NOW(), '1 hour')) AS bucket,
device_id,
COALESCE(AVG(temperature), 0) AS interp_temp,
interpolate(gaps(temperature)) AS filled_temp
FROM metrics
GROUP BY 1, 2
ORDER BY 1, 2;lttb reduces points while preserving curve shape; gaps/interpolate fills holes. Perfect for alerting. Requires the extension enabled. Avoid on recent uncompressed data for performance.
Best Practices
- Chunk sizing: Aim for 25-50M rows/chunk with
chunk_time_intervalbased on ingestion rate (e.g., 1h for 1k/sec). - Indexing: Add BRIN on time + GIN on JSONB tags after hypertable creation.
- Monitoring: Query
timescaledb_information.hypertableandchunksdaily; alert if >100 active chunks. - Scaling: Use distributed hypertables for multi-node with
create_distributed_hypertable. - Backup: pg_dumpall + WAL for chunks; WAL-E for S3.
Common Errors to Avoid
- No ORDER BY in compression: Degrades sequential decompression, +50% CPU.
- Too-small chunks: Metadata overhead; monitor via
timescaledb_information.chunks. - Missing end_offset in cont. aggs: Real-time lag up to bucket size.
- Writes on compressed chunks: Forces decompression; segment by TTL.
Next Steps
- Official docs: TimescaleDB Documentation
- Multi-node: Distributed Hypertables
- Kafka integration: Via
timescaledb_toolkit - Expert training: Discover our advanced PostgreSQL courses at Learni
- Community: TimescaleDB GitHub and PostgreSQL forums.