Time-Series Databases
Optimized storage and queries for timestamped data at scale
When Regular Databases Struggle with Time
Storing billions of sensor readings, server metrics, stock prices, or IoT events in PostgreSQL or MySQL creates performance problems: queries slow to a crawl, indexes become massive, storage explodes, and common queries like "show me the average CPU usage per hour for the last 30 days" take minutes instead of milliseconds. Time-series databasessolve these problems with specialized storage engines, compression algorithms, and query optimizations designed for timestamped data. This lesson covers TimescaleDB(PostgreSQL extension for time-series), InfluxDB (purpose-built for metrics), and Prometheus (monitoring-focused). You'll learn when to use each, how retention policies automatically delete old data, how downsampling reduces storage by 90%+, and the query patterns that make time-series queries blazingly fast.
What Makes Time-Series Data Different?
Time-series data has unique characteristics that make traditional databases inefficient: write-heavy workloads, append-only patterns, time-based queries, and massive volumes.
Write-Heavy, Append-Only
- Constant stream of new data
- Rarely update historical data
- No random updates or deletes
- Example: 10,000 sensors × 1 reading/sec = 864M writes/day
Time-Based Queries
- Queries always filter by time range
- Aggregations over time windows
- Recent data queried more often
- Example: "CPU usage last 24 hours"
Massive Volume
- Billions of data points
- Storage grows continuously
- Old data less valuable
- Example: 1 year = 315 billion readings
Tagged/Labeled Data
- Metadata tags (server, region, sensor_id)
- High cardinality dimensions
- Queries filter by tags + time
- Example: server="web-1", region="us-east"
Traditional Database Problem:
Table: sensor_readings (1 billion rows)
┌────────────┬───────────┬─────────┬───────────┐
│ timestamp │ sensor_id │ value │ location │
├────────────┼───────────┼─────────┼───────────┤
│ 2024-06-15 │ sensor_1 │ 23.5 │ building_a│
│ 2024-06-15 │ sensor_2 │ 24.1 │ building_a│
│ ... │ ... │ ... │ ... │
└────────────┴───────────┴─────────┴───────────┘
Query: SELECT AVG(value) FROM sensor_readings
WHERE timestamp >= NOW() - INTERVAL '24 hours'
AND sensor_id = 'sensor_1'
Problem:
• Table scan of 1 billion rows (slow!)
• Index on (timestamp, sensor_id) huge
• No compression (each row stored separately)
• Query time: 30+ seconds
Time-Series Database Solution:
• Automatic partitioning by time (only scan last day)
• Columnar compression (delta encoding for values)
• Pre-aggregated rollups (hourly averages cached)
• Query time: < 100ms (300x faster!)TimescaleDB: PostgreSQL Extension for Time-Series
TimescaleDB extends PostgreSQL with automatic partitioning (hypertables), compression, and time-series-specific functions. You get time-series performance while keeping full SQL compatibility and PostgreSQL's reliability.
Setting Up TimescaleDB
-- Install TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Create regular PostgreSQL table
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
sensor_id TEXT NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
location TEXT
);
-- Convert to hypertable (automatic time-based partitioning)
SELECT create_hypertable('sensor_data', 'time');
-- Create indexes for common queries
CREATE INDEX ON sensor_data (sensor_id, time DESC);
CREATE INDEX ON sensor_data (location, time DESC);Inserting Time-Series Data with Python
import psycopg2
from datetime import datetime, timedelta
import random
# Connect to TimescaleDB (PostgreSQL with extension)
conn = psycopg2.connect(
dbname="timeseries_db",
user="postgres",
host="localhost"
)
cursor = conn.cursor()
# Simulate sensor data
sensors = ['sensor_1', 'sensor_2', 'sensor_3']
locations = ['building_a', 'building_b', 'building_c']
print("Inserting 10,000 sensor readings...")
base_time = datetime.now() - timedelta(days=7)
for i in range(10000):
timestamp = base_time + timedelta(minutes=i)
sensor_id = random.choice(sensors)
location = random.choice(locations)
temperature = round(random.uniform(18.0, 28.0), 2)
humidity = round(random.uniform(30.0, 70.0), 2)
cursor.execute("""
INSERT INTO sensor_data (time, sensor_id, temperature, humidity, location)
VALUES (%s, %s, %s, %s, %s)
""", (timestamp, sensor_id, temperature, humidity, location))
if (i + 1) % 1000 == 0:
print(f" Inserted {i + 1} records...")
conn.commit()
conn.commit()
print("✓ Data insertion complete!")
cursor.close()
conn.close()Inserting 10,000 sensor readings...
Inserted 1000 records...
Inserted 2000 records...
...
✓ Data insertion complete!
Time-Series Queries
import psycopg2
from datetime import datetime, timedelta
conn = psycopg2.connect(dbname="timeseries_db", user="postgres")
cursor = conn.cursor()
# Query 1: Recent data (last 24 hours)
cursor.execute("""
SELECT
sensor_id,
AVG(temperature) as avg_temp,
MAX(temperature) as max_temp,
MIN(temperature) as min_temp,
COUNT(*) as reading_count
FROM sensor_data
WHERE time >= NOW() - INTERVAL '24 hours'
GROUP BY sensor_id
ORDER BY sensor_id
""")
print("Sensor statistics (last 24 hours):")
for sensor, avg_t, max_t, min_t, count in cursor.fetchall():
print(f" {sensor}: Avg={avg_t:.1f}°C, Max={max_t:.1f}°C, Min={min_t:.1f}°C ({count} readings)")
# Query 2: Time-bucket aggregation (hourly averages)
cursor.execute("""
SELECT
time_bucket('1 hour', time) AS hour,
sensor_id,
AVG(temperature) as avg_temp,
AVG(humidity) as avg_humidity
FROM sensor_data
WHERE time >= NOW() - INTERVAL '7 days'
GROUP BY hour, sensor_id
ORDER BY hour DESC, sensor_id
LIMIT 10
""")
print("\nHourly averages (last 7 days, most recent first):")
for hour, sensor, temp, humidity in cursor.fetchall():
print(f" {hour} - {sensor}: {temp:.1f}°C, {humidity:.1f}%")
cursor.close()
conn.close()Sensor statistics (last 24 hours):
sensor_1: Avg=23.2°C, Max=27.8°C, Min=18.5°C (485 readings)
sensor_2: Avg=22.8°C, Max=26.9°C, Min=19.1°C (478 readings)
sensor_3: Avg=23.5°C, Max=27.5°C, Min=18.8°C (492 readings)
Hourly averages (last 7 days, most recent first):
2024-06-15 14:00:00 - sensor_1: 23.4°C, 52.3%
2024-06-15 14:00:00 - sensor_2: 22.9°C, 48.7%
...
Compression (Reduce Storage by 90%)
-- Enable compression on chunks older than 7 days
ALTER TABLE sensor_data SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id, location',
timescaledb.compress_orderby = 'time DESC'
);
-- Add compression policy (automatic)
SELECT add_compression_policy('sensor_data', INTERVAL '7 days');
-- Manually compress specific chunks (for demo)
SELECT compress_chunk(i)
FROM show_chunks('sensor_data', older_than => INTERVAL '7 days') i;Checking Compression Results
import psycopg2
conn = psycopg2.connect(dbname="timeseries_db", user="postgres")
cursor = conn.cursor()
# Check compression statistics
cursor.execute("""
SELECT
pg_size_pretty(before_compression_total_bytes) as uncompressed,
pg_size_pretty(after_compression_total_bytes) as compressed,
ROUND(100 - (after_compression_total_bytes::numeric /
before_compression_total_bytes::numeric * 100), 2) as compression_ratio
FROM hypertable_compression_stats('sensor_data')
""")
result = cursor.fetchone()
if result:
uncompressed, compressed, ratio = result
print("Compression Statistics:")
print(f" Uncompressed size: {uncompressed}")
print(f" Compressed size: {compressed}")
print(f" Space saved: {ratio}%")
else:
print("No compression data available yet")
cursor.close()
conn.close()Compression Statistics:
Uncompressed size: 156 MB
Compressed size: 18 MB
Space saved: 88.46%
Retention Policies (Auto-Delete Old Data)
-- Automatically drop data older than 90 days
SELECT add_retention_policy('sensor_data', INTERVAL '90 days');
-- Check retention policy
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';InfluxDB: Purpose-Built Time-Series Database
InfluxDB is designed from the ground up for time-series data. It uses its own query language (Flux/InfluxQL), provides built-in visualization, and excels at high-cardinality data (millions of unique tag combinations).
InfluxDB Data Model
InfluxDB Data Structure: measurement (like a table) │ ├─ tags (indexed, metadata) │ ├─ sensor_id = "sensor_1" │ ├─ location = "building_a" │ └─ region = "us-east" │ ├─ fields (actual data, not indexed) │ ├─ temperature = 23.5 │ └─ humidity = 52.3 │ └─ timestamp = 2024-06-15T10:30:00Z Example data point: ┌─────────────────────────────────────────────────┐ │ Measurement: sensor_readings │ ├─────────────────────────────────────────────────┤ │ Tags: │ │ sensor_id=sensor_1, location=building_a │ │ Fields: │ │ temperature=23.5, humidity=52.3 │ │ Timestamp: 2024-06-15T10:30:00Z │ └─────────────────────────────────────────────────┘ Key Concepts: • Measurement = table name • Tags = indexed dimensions (WHERE filters) • Fields = actual values (SELECT columns) • Timestamp = automatically indexed
Writing Data to InfluxDB
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
from datetime import datetime, timedelta
import random
# Connect to InfluxDB
client = InfluxDBClient(
url="http://localhost:8086",
token="your-token-here",
org="my-org"
)
write_api = client.write_api(write_options=SYNCHRONOUS)
bucket = "sensor_data"
# Write sensor readings
print("Writing sensor data to InfluxDB...")
sensors = ['sensor_1', 'sensor_2', 'sensor_3']
locations = ['building_a', 'building_b']
base_time = datetime.utcnow() - timedelta(hours=24)
for i in range(1000):
timestamp = base_time + timedelta(minutes=i)
for sensor in sensors:
for location in locations:
point = Point("sensor_readings") \
.tag("sensor_id", sensor) \
.tag("location", location) \
.field("temperature", round(random.uniform(18.0, 28.0), 2)) \
.field("humidity", round(random.uniform(30.0, 70.0), 2)) \
.time(timestamp)
write_api.write(bucket=bucket, record=point)
if (i + 1) % 100 == 0:
print(f" Written {(i + 1) * len(sensors) * len(locations)} points...")
print("✓ Data written successfully!")
client.close()Writing sensor data to InfluxDB...
Written 600 points...
Written 1200 points...
...
✓ Data written successfully!
Querying with Flux
from influxdb_client import InfluxDBClient
client = InfluxDBClient(
url="http://localhost:8086",
token="your-token-here",
org="my-org"
)
query_api = client.query_api()
# Query 1: Average temperature by sensor (last 24 hours)
query = '''
from(bucket: "sensor_data")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "sensor_readings")
|> filter(fn: (r) => r._field == "temperature")
|> group(columns: ["sensor_id"])
|> mean()
'''
tables = query_api.query(query)
print("Average temperature by sensor (last 24h):")
for table in tables:
for record in table.records:
sensor = record.values.get("sensor_id")
avg_temp = record.get_value()
print(f" {sensor}: {avg_temp:.2f}°C")
# Query 2: Downsampled data (hourly averages)
query = '''
from(bucket: "sensor_data")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "sensor_readings")
|> filter(fn: (r) => r.sensor_id == "sensor_1")
|> filter(fn: (r) => r._field == "temperature")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> limit(n: 10)
'''
tables = query_api.query(query)
print("\nHourly temperature averages for sensor_1:")
for table in tables:
for record in table.records:
time = record.get_time()
temp = record.get_value()
print(f" {time}: {temp:.2f}°C")
client.close()Average temperature by sensor (last 24h):
sensor_1: 23.14°C
sensor_2: 22.87°C
sensor_3: 23.42°C
Hourly temperature averages for sensor_1:
2024-06-15 14:00:00: 23.45°C
2024-06-15 13:00:00: 22.89°C
...
Downsampling (Continuous Queries)
# Create downsampling task (runs every hour)
# Reduces high-resolution data to hourly averages
from influxdb_client import InfluxDBClient
from influxdb_client.client.tasks_api import TasksApi
client = InfluxDBClient(url="http://localhost:8086", token="your-token-here", org="my-org")
tasks_api = TasksApi(client)
# Task: Calculate hourly averages and store in separate bucket
task_flux = '''
option task = {name: "downsample_hourly", every: 1h}
from(bucket: "sensor_data")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor_readings")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> to(bucket: "sensor_data_hourly")
'''
task = tasks_api.create_task_every(
name="downsample_hourly",
flux=task_flux,
every="1h",
organization="my-org"
)
print(f"Created downsampling task: {task.name}")
print(f" Runs every: {task.every}")
print(" Purpose: Reduce raw data to hourly averages")
client.close()Created downsampling task: downsample_hourly
Runs every: 1h
Purpose: Reduce raw data to hourly averages
Retention Policies in InfluxDB
from influxdb_client import InfluxDBClient
from influxdb_client.client.buckets_api import BucketsApi, RetentionRules
client = InfluxDBClient(url="http://localhost:8086", token="your-token-here", org="my-org")
buckets_api = BucketsApi(client)
# Create bucket with 30-day retention for raw data
retention_rules = RetentionRules(type="expire", every_seconds=30 * 24 * 60 * 60) # 30 days
raw_bucket = buckets_api.create_bucket(
bucket_name="sensor_data_raw",
retention_rules=retention_rules,
org="my-org"
)
print("Created buckets with retention policies:")
print(f" Raw data (sensor_data_raw): 30 days retention")
# Create bucket with 1-year retention for downsampled data
retention_rules_long = RetentionRules(type="expire", every_seconds=365 * 24 * 60 * 60) # 1 year
hourly_bucket = buckets_api.create_bucket(
bucket_name="sensor_data_hourly",
retention_rules=retention_rules_long,
org="my-org"
)
print(f" Downsampled data (sensor_data_hourly): 1 year retention")
print("\n✓ Old data automatically deleted after retention period!")
client.close()Created buckets with retention policies:
Raw data (sensor_data_raw): 30 days retention
Downsampled data (sensor_data_hourly): 1 year retention
✓ Old data automatically deleted after retention period!
Prometheus: Monitoring-Focused Time-Series DB
Prometheus is designed specifically for monitoring and alerting. It uses a pull-based model (scrapes metrics from targets), has a powerful query language (PromQL), and integrates seamlessly with Grafana for visualization.
Prometheus Data Model
Prometheus Metrics:
Metric name + labels = time series
Example:
http_requests_total{method="GET", endpoint="/api/users", status="200"} 12345
Components:
• http_requests_total = metric name
• {method="GET", endpoint="/api/users", status="200"} = labels
• 12345 = value at this timestamp
Metric Types:
1. Counter: Cumulative value (only increases)
- http_requests_total
- errors_total
2. Gauge: Value that can go up or down
- memory_usage_bytes
- active_connections
3. Histogram: Distribution of values
- request_duration_seconds
- response_size_bytes
4. Summary: Similar to histogram (percentiles)
- api_latency_secondsExposing Metrics with Python
from prometheus_client import start_http_server, Counter, Gauge, Histogram
import random
import time
# Define metrics
request_counter = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
active_users = Gauge(
'active_users',
'Number of active users'
)
request_duration = Histogram(
'request_duration_seconds',
'Request duration in seconds',
['endpoint']
)
# Simulate application metrics
def simulate_traffic():
"""Simulate web application traffic"""
endpoints = ['/api/users', '/api/products', '/api/orders']
methods = ['GET', 'POST']
statuses = ['200', '404', '500']
while True:
# Increment request counter
endpoint = random.choice(endpoints)
method = random.choice(methods)
status = random.choice(statuses)
request_counter.labels(method=method, endpoint=endpoint, status=status).inc()
# Update active users gauge
active_users.set(random.randint(100, 500))
# Record request duration
duration = random.uniform(0.01, 2.0)
request_duration.labels(endpoint=endpoint).observe(duration)
time.sleep(1) # Wait 1 second
if __name__ == '__main__':
# Start metrics server on port 8000
start_http_server(8000)
print("Metrics server started on port 8000")
print("Prometheus can scrape: http://localhost:8000/metrics")
# Simulate traffic
simulate_traffic()Metrics server started on port 8000
Prometheus can scrape: http://localhost:8000/metrics
Visit http://localhost:8000/metrics to see exported metrics
Querying with PromQL
from prometheus_api_client import PrometheusConnect
import pandas as pd
# Connect to Prometheus server
prom = PrometheusConnect(url="http://localhost:9090", disable_ssl=True)
# Query 1: Current request rate (requests per second)
query = 'rate(http_requests_total[5m])'
result = prom.custom_query(query)
print("Current request rate (requests/sec):")
for item in result:
metric = item['metric']
value = float(item['value'][1])
print(f" {metric['endpoint']} ({metric['method']}): {value:.2f} req/s")
# Query 2: 95th percentile request duration
query = 'histogram_quantile(0.95, rate(request_duration_seconds_bucket[5m]))'
result = prom.custom_query(query)
print("\n95th percentile request duration:")
for item in result:
endpoint = item['metric'].get('endpoint', 'unknown')
p95 = float(item['value'][1])
print(f" {endpoint}: {p95:.3f}s")
# Query 3: Error rate (percentage)
query = '''
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) * 100
'''
result = prom.custom_query(query)
if result:
error_rate = float(result[0]['value'][1])
print(f"\nCurrent error rate: {error_rate:.2f}%")Current request rate (requests/sec):
/api/users (GET): 12.45 req/s
/api/products (GET): 8.23 req/s
/api/orders (POST): 3.67 req/s
95th percentile request duration:
/api/users: 0.234s
/api/products: 0.189s
/api/orders: 0.456s
Current error rate: 2.34%
Prometheus Configuration (prometheus.yml)
# prometheus.yml configuration
global:
scrape_interval: 15s # How often to scrape targets
evaluation_interval: 15s # How often to evaluate rules
# Scrape configurations
scrape_configs:
# Job 1: Python application metrics
- job_name: 'python_app'
static_configs:
- targets: ['localhost:8000']
labels:
environment: 'production'
app: 'sensor_api'
# Job 2: Node exporter (system metrics)
- job_name: 'node'
static_configs:
- targets: ['localhost:9100']
# Job 3: PostgreSQL metrics
- job_name: 'postgres'
static_configs:
- targets: ['localhost:9187']
# Storage retention (configured via CLI flag, not in this file):
# prometheus --storage.tsdb.retention.time=15dUse Cases: Which Database to Choose?
Each time-series database excels in different scenarios. Choose based on your specific requirements and existing infrastructure.
| Use Case | Best Choice | Why |
|---|---|---|
| IoT sensor data | TimescaleDB | SQL queries, relational joins with device metadata, PostgreSQL reliability |
| Application monitoring | Prometheus | Pull-based metrics, alerting built-in, Grafana integration |
| Financial tick data | TimescaleDB | ACID guarantees, SQL for complex analysis, continuous aggregates |
| DevOps metrics | Prometheus | Industry standard, service discovery, powerful alerting |
| High-cardinality data | InfluxDB | Optimized for millions of unique tag combinations |
| Network telemetry | InfluxDB | Handles high write throughput, built-in downsampling |
| Business analytics | TimescaleDB | SQL for BI tools, joins with dimensional tables, PostgreSQL ecosystem |
| Kubernetes monitoring | Prometheus | Native Kubernetes integration, automatic service discovery |
| Multi-tenant SaaS metrics | InfluxDB | Tag-based isolation, enterprise features, cloud-native |
| Energy consumption tracking | TimescaleDB | Complex queries, reporting, regulatory compliance (SQL audit trails) |
TimescaleDB
Best for:
- SQL familiarity important
- Need ACID guarantees
- Complex joins required
- PostgreSQL ecosystem
Pros:
- Full SQL support
- PostgreSQL reliability
- Rich ecosystem
InfluxDB
Best for:
- High write throughput
- High cardinality
- Cloud-native deployment
- Built-in visualization
Pros:
- Purpose-built for TS
- Excellent compression
- Flux language power
Prometheus
Best for:
- Monitoring & alerting
- Kubernetes environments
- DevOps workflows
- Service discovery
Pros:
- Industry standard
- Powerful PromQL
- Great ecosystem
Downsampling and Retention Strategies
Time-series data grows continuously. Downsampling reduces resolution over time (hourly → daily → weekly), while retention policies automatically delete old data to manage storage costs.
Multi-Tier Retention Strategy
Typical Multi-Tier Retention: Tier 1: Raw Data (Full Resolution) ┌─────────────────────────────────────────┐ │ Retention: 7 days │ │ Granularity: 1 second │ │ Storage: 100 GB │ │ Use: Recent detailed analysis │ └─────────────────────────────────────────┘ Tier 2: Hourly Aggregates ┌─────────────────────────────────────────┐ │ Retention: 90 days │ │ Granularity: 1 hour │ │ Storage: 5 GB (95% reduction!) │ │ Use: Weekly/monthly reports │ └─────────────────────────────────────────┘ Tier 3: Daily Aggregates ┌─────────────────────────────────────────┐ │ Retention: 2 years │ │ Granularity: 1 day │ │ Storage: 500 MB (99.5% reduction!) │ │ Use: Long-term trend analysis │ └─────────────────────────────────────────┘ Benefits: • Keep detailed data when it matters (recent) • Reduce storage costs (compress old data) • Fast queries (pre-aggregated) • Compliance (retain summaries long-term)
Implementing Multi-Tier Retention (TimescaleDB)
-- Create continuous aggregate for hourly data
CREATE MATERIALIZED VIEW sensor_data_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS hour,
sensor_id,
location,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(humidity) as avg_humidity,
COUNT(*) as reading_count
FROM sensor_data
GROUP BY hour, sensor_id, location;
-- Create continuous aggregate for daily data
CREATE MATERIALIZED VIEW sensor_data_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', time) AS day,
sensor_id,
location,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(humidity) as avg_humidity,
COUNT(*) as reading_count
FROM sensor_data
GROUP BY day, sensor_id, location;
-- Retention policies
SELECT add_retention_policy('sensor_data', INTERVAL '7 days'); -- Raw: 7 days
SELECT add_retention_policy('sensor_data_hourly', INTERVAL '90 days'); -- Hourly: 90 days
SELECT add_retention_policy('sensor_data_daily', INTERVAL '2 years'); -- Daily: 2 years
-- Compression policies
SELECT add_compression_policy('sensor_data', INTERVAL '1 day');
SELECT add_compression_policy('sensor_data_hourly', INTERVAL '7 days');Continuous aggregates automatically maintained
Retention policies automatically delete old data
Compression policies reduce storage by 90%+
Queries automatically use the most efficient view
Querying Across Retention Tiers
import psycopg2
from datetime import datetime, timedelta
conn = psycopg2.connect(dbname="timeseries_db", user="postgres")
cursor = conn.cursor()
# Query recent data (uses raw table - high resolution)
cursor.execute("""
SELECT
time_bucket('5 minutes', time) as interval,
AVG(temperature) as avg_temp
FROM sensor_data
WHERE time >= NOW() - INTERVAL '6 hours'
AND sensor_id = 'sensor_1'
GROUP BY interval
ORDER BY interval DESC
LIMIT 10
""")
print("Recent data (5-minute intervals, last 6 hours):")
for interval, temp in cursor.fetchall():
print(f" {interval}: {temp:.2f}°C")
# Query older data (uses hourly aggregate - pre-computed)
cursor.execute("""
SELECT
hour,
avg_temperature
FROM sensor_data_hourly
WHERE hour >= NOW() - INTERVAL '30 days'
AND hour < NOW() - INTERVAL '7 days'
AND sensor_id = 'sensor_1'
ORDER BY hour DESC
LIMIT 10
""")
print("\nOlder data (hourly averages, 7-30 days ago):")
for hour, temp in cursor.fetchall():
print(f" {hour}: {temp:.2f}°C")
# Query historical data (uses daily aggregate)
cursor.execute("""
SELECT
day,
avg_temperature,
max_temperature,
min_temperature
FROM sensor_data_daily
WHERE day >= NOW() - INTERVAL '1 year'
AND sensor_id = 'sensor_1'
ORDER BY day DESC
LIMIT 10
""")
print("\nHistorical data (daily averages, last year):")
for day, avg_t, max_t, min_t in cursor.fetchall():
print(f" {day}: Avg={avg_t:.1f}°C, Max={max_t:.1f}°C, Min={min_t:.1f}°C")
cursor.close()
conn.close()Recent data (5-minute intervals, last 6 hours):
2024-06-15 14:25:00: 23.45°C
2024-06-15 14:20:00: 23.42°C
...
Older data (hourly averages, 7-30 days ago):
2024-05-15 14:00:00: 22.87°C
2024-05-15 13:00:00: 23.12°C
...
Historical data (daily averages, last year):
2023-06-15: Avg=22.5°C, Max=28.3°C, Min=18.7°C
...
Key Takeaways
- Time-series data: Timestamped, write-heavy, append-only
- TimescaleDB: PostgreSQL extension, SQL queries, ACID guarantees
- InfluxDB: Purpose-built, high cardinality, Flux queries
- Prometheus: Monitoring-focused, pull-based, PromQL
- Compression: Reduces storage by 90%+ using delta encoding
- Downsampling: Reduce resolution over time (hourly → daily)
- Retention policies: Auto-delete old data to manage costs
- Multi-tier: Keep raw recent, aggregated historical data