Skip to content

Repository files navigation

Time Series Databases


What It Studies

Time series databases are purpose-built for append-heavy, timestamp-indexed workloads. This project answers the question nobody asks clearly enough: what does a general purpose database actually get wrong for time series data, and at what point does a specialised engine start to matter?

The answer is not binary. It depends entirely on the query pattern.


The Three-Way Benchmark

The same 18,144,000-row IoT sensor dataset is loaded into three databases. The same 5 queries are run against all three. Results are timed, compared, and explained.

DatabaseTypePort
Vanilla PostgreSQL 15General purpose RDBMS — the baseline5432
TimescaleDB (PG15 + extension)PostgreSQL with hypertable + continuous aggregate5433
InfluxDB 2.7Purpose-built TSDB, Flux query language8086

Dataset

30 IoT sensors at 1-second intervals over 7 days.

device_id string sensor_001 … sensor_030
ts timestamptz 1-second intervals, UTC
temperature float Gaussian per device baseline ± noise
humidity float Gaussian per device baseline ± noise
pressure float Gaussian per device baseline ± noise
battery_pct float linear drain 100% → ~70% over 7 days
is_anomaly bool 1% of readings flagged

Total rows: 18,144,000 (30 devices × 604,800 seconds) File size: ~180MB (Snappy-compressed Parquet) Seed: 42 — fully reproducible


Directory Structure

time-series-databases/
├── docker-compose.yml # postgres_vanilla, timescaledb, influxdb
├── requirements.txt
├── generate_sensor.py # generates data/sensors.parquet
├── ingest_postgres.py # vanilla PG — COPY + btree indexes
├── ingest_timescale.py # hypertable + continuous aggregate + retention
├── ingest_influx.py # line protocol write to InfluxDB
├── benchmark_queries.py # 5 queries × 3 databases, timed comparison
└── data/ # gitignored — sensors.parquet, benchmark_results.json

Run Order

# 1. Start containers
docker compose up -d
# 2. Retrieve InfluxDB token (generated at container init)
docker exec influxdb influx auth list --hide-headers | awk '{print $4}'export INFLUX_TOKEN="your-token-here"# 3. Environment
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
# 4. Generate dataset
python generate_sensor.py
# 5. Ingest into all three databases
python ingest_postgres.py
python ingest_timescale.py
python ingest_influx.py
# 6. Run benchmark
python benchmark_queries.py
# 7. Teardown
docker compose down # stop, preserve volumes
docker compose down -v # stop, destroy volumes (full reset)# Note: docker compose down -v destroys the InfluxDB token — retrieve a new one on next start

The 5 Benchmark Queries

#QueryWhat it tests
Q1Last value per devicePoint lookup — latest reading for all 30 devices
Q25-min rolling average (sensor_001, 1h window)Time-windowed aggregation
Q3Gap detection — missing readings > 2 minTemporal reasoning across full dataset
Q4Hourly downsample (7 days)Aggregation — the critical query
Q5Range scan — sensor_001, full daySelective time predicate, 86,400 rows

Each query runs 3 times per database. Best time is recorded.


Results

Three-Way Comparison

QueryVanilla PGTimescaleDBInfluxDBWinner
Q1 Last value2.594s9.296s0.031sInfluxDB ✓
Q2 Rolling avg0.078s0.097s0.013sInfluxDB ✓
Q3 Gap detection17.557s13.991sERRORTimescaleDB ✓
Q4 Hourly downsample0.206s0.003s0.349sTimescaleDB ✓
Q5 Range scan0.194s0.320sERRORVanilla PG ✓

Speedup Over Vanilla PostgreSQL Baseline

QueryTimescaleDBInfluxDB
Q10.3× (slower)83.9× faster
Q20.8× (slower)5.9× faster
Q31.3× faster— (timeout)
Q476.4× faster0.6× (slower)
Q50.6× (slower)— (error)

Key Findings

No single database wins every query. The right tool depends on the dominant query pattern in your workload.

TimescaleDB's continuous aggregate is the standout result. Q4 at 76.4× over vanilla PG is not a marginal improvement — it is a different order of magnitude. The continuous aggregate pre-computes hourly aggregates incrementally as data arrives. At query time TimescaleDB reads a materialised view, not 18M raw rows.

InfluxDB dominates point lookups.last() is a native primitive. Q1 at 83.9× over vanilla PG shows what purpose-built means — the engine was designed from the ground up to answer "what is the latest value" instantly.

TimescaleDB's hypertable can be slower than vanilla PG. Q1 and Q5 both show TimescaleDB slower than vanilla PG on the same query. For point lookups and selective single-device range scans, the chunk file overhead exceeds the pruning benefit. Hypertables win at wide time ranges and high cardinality, not on narrow selective queries.

Vanilla PG is not the wrong tool. On Q5 it beats TimescaleDB. On Q2 it is within 25% of TimescaleDB. A well-indexed PostgreSQL table handles time series workloads at moderate scale — the TSDB argument only becomes compelling at the specific query patterns where specialisation matters.

InfluxDB fails at cross-series temporal analysis. Q3 gap detection timed out. InfluxDB is optimised for tag-filtered, field-scoped queries within bounded time ranges. Full dataset temporal reasoning is outside its sweet spot.


TimescaleDB Concepts

Hypertable — a standard PostgreSQL table whose storage is automatically partitioned into time-based chunks. A query with a time predicate only opens the relevant chunk files. Queries without time predicates must open all chunks — which is why Q1 is slower on TimescaleDB than vanilla PG.

Continuous aggregate — a materialised view that recomputes incrementally on a background schedule. sensor_readings_hourly stores pre-computed hourly averages per device. Q4 reads from this view rather than aggregating 18M rows live.

Retention policy — automatic chunk deletion after N days at the filesystem level. O(1) regardless of row count. Vanilla PG DELETE WHERE ts < cutoff is O(n).

InfluxDB Concepts

Tags vs Fields — tags are indexed string metadata (device_id). Fields are the actual numeric values (temperature, humidity). Filtering on tags is fast. Filtering on field values is a full scan. Getting this wrong is the most common InfluxDB production mistake.

Line protocolmeasurement,tag=value field=value timestamp_ns. The wire format for writes. Built for high-throughput appends.

Flux — InfluxDB's functional query language. Every query must start with range() — InfluxDB always filters by time first. Queries without a time bound are rejected.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages