Skip to content
Learni
View all tutorials
Bases de données

How to Master TimescaleDB in 2026

Lire en français

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

docker-compose.yml
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: local

This 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_hypertable.sql
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

queries.sql
-- 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

continuous_agg.sql
-- 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

compression.sql
-- 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

retention.sql
-- 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

hyperfunctions.sql
-- 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_interval based 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.hypertable and chunks daily; 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