Stage 3 · Build
NoSQL & Distributed Databases
Time-Series Databases
InfluxDB, TimescaleDB — data model, retention policies, and downsampling.
Why Time-Series Databases?
Time-series data is append-heavy, time-ordered, and queried by time ranges. Metrics, logs, events, and IoT sensor data are all time-series. General-purpose databases handle this adequately, but specialized time-series databases optimize for the specific access patterns.
| Feature | PostgreSQL | TimescaleDB | InfluxDB |
|---|---|---|---|
| Data model | Relational | Relational (hypertables) | Measurement/field/tag |
| Query language | SQL | SQL | InfluxQL/Flux |
| Compression | None | Automatic columnar | Automatic |
| Retention | Manual | Continuous aggregates | Built-in retention |
| Best for | Mixed workloads | PostgreSQL users needing TS | Pure metrics/IoT |
TimescaleDB (PostgreSQL Extension)
-- Install extension
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Create a regular table
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
sensor_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
battery_level INTEGER
);
-- Convert to hypertable (partitioned by time)
SELECT create_hypertable('sensor_data', 'time');
-- Insert data (works like regular PostgreSQL)
INSERT INTO sensor_data VALUES
(NOW(), 1, 72.5, 45.0, 85),
(NOW() - INTERVAL '1 hour', 1, 71.2, 46.0, 84);
-- Query with time bucketing
SELECT
time_bucket('1 hour', time) AS hour,
AVG(temperature) AS avg_temp,
MAX(humidity) AS max_humidity
FROM sensor_data
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour;TimescaleDB automatically partitions data into chunks based on time. The time_bucket function is the equivalent of date_trunc but more flexible — you can bucket by any interval.
InfluxDB
# Write data point (line protocol)
curl -XPOST 'http://localhost:8086/write?db=metrics' \
-d 'cpu,host=server01,region=us-west usage_idle=82.3,usage_user=15.1 1718450000000000000'
# Query with InfluxQL
curl -G 'http://localhost:8086/query?db=metrics' \
--data-urlencode 'q=SELECT mean("usage_idle") FROM "cpu" WHERE time > now() - 1h GROUP BY time(5m)'
# Query with Flux (newer, more powerful)
curl -XPOST 'http://localhost:8086/api/v2/query' \
-d 'from(bucket: "metrics")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu")
|> aggregateWindow(every: 5m, fn: mean)'InfluxDB uses a measurement/tag/field model. Measurements are like tables, tags are indexed metadata (host, region), and fields are the actual data points. Tags are for filtering, fields are for computation.
Retention Policies
-- Drop data older than 90 days
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');
-- Check retention policies
SELECT * FROM timescaledb_information.policies;
-- Drop retention policy
SELECT remove_retention_policy('sensor_data');Retention policies automatically delete old data. This is essential for time-series data that grows unbounded. Set retention based on compliance requirements and storage budget.
# Create retention policy (30 days)
curl -XPOST 'http://localhost:8086/query' \
-d 'CREATE RETENTION POLICY "30d" ON "metrics" DURATION 30d REPLICATION 1'
# Create infinite retention policy (default)
curl -XPOST 'http://localhost:8086/query' \
-d 'CREATE RETENTION POLICY "forever" ON "metrics" DURATION INF REPLICATION 1'
# Set default retention policy
curl -XPOST 'http://localhost:8086/query' \
-d 'ALTER RETENTION POLICY "30d" ON "metrics" DEFAULT'InfluxDB retention policies are per-database. Data older than the retention duration is automatically dropped during compaction. You can have multiple retention policies for different data tiers.
Downsampling and Continuous Aggregates
-- Create continuous aggregate (materialized view that auto-refreshes)
CREATE MATERIALIZED VIEW hourly_sensor_data
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS hour,
sensor_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp,
MIN(temperature) AS min_temp,
AVG(humidity) AS avg_humidity
FROM sensor_data
GROUP BY hour, sensor_id;
-- Add refresh policy (refresh every 5 minutes)
SELECT add_continuous_aggregate_policy('hourly_sensor_data',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '5 minutes'
);
-- Add retention policy to continuous aggregate
SELECT add_retention_policy('hourly_sensor_data', INTERVAL '1 year');Continuous aggregates pre-compute expensive aggregations. Queries against hourly_sensor_data are 100-1000x faster than querying raw data. The policy automatically refreshes as new data arrives.
Keep raw data for 7-30 days, hourly aggregates for 1 year, daily aggregates forever. This tiered approach gives you detailed data for recent debugging while keeping storage costs manageable for historical trends.
Time-Series Query Patterns
-- Last N minutes of data
SELECT * FROM sensor_data
WHERE time > NOW() - INTERVAL '10 minutes'
AND sensor_id = 1;
-- Time-weighted average (interpolation between points)
SELECT
time_bucket('5 minutes', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temp
FROM sensor_data
WHERE time > NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id;
-- Detect anomalies (values outside 2 standard deviations)
WITH stats AS (
SELECT
AVG(temperature) AS mean,
STDDEV(temperature) AS stddev
FROM sensor_data
WHERE time > NOW() - INTERVAL '24 hours'
)
SELECT *
FROM sensor_data, stats
WHERE ABS(temperature - mean) > 2 * stddev
AND time > NOW() - INTERVAL '1 hour';
-- Gap detection (missing data points)
SELECT time, sensor_id
FROM sensor_data
WHERE time > NOW() - INTERVAL '1 hour'
AND sensor_id = 1
AND time - LAG(time) OVER (ORDER BY time) > INTERVAL '5 minutes';Time-series queries often involve: time range filtering, aggregation by time bucket, gap detection, and anomaly detection. These patterns work across both TimescaleDB and InfluxDB with different syntax.
Choose TimescaleDB if you already use PostgreSQL and want SQL compatibility. Choose InfluxDB if you need a pure metrics store with built-in visualization (Grafana) and alerting. Both handle millions of data points per second.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.