Engineering: backend-engineering, frontend-engineering, data-engineering, ml-engineering, platform-engineering, qa-methodology Executive: go-to-market, legal-strategy, operational-design, org-design, product-strategy ml-engineering: added missing training-infrastructure.md reference qa-methodology: added test-data-management, performance-testing, security-testing references All frontmatter converted to agent-skills convention. Source: https://github.com/magnus919/hermes-profiles
35 KiB
Time-Series Database Patterns for Data Engineering
A thorough reference on time-series databases with a focus on InfluxDB, written for the data engineering methodology skill.
Table of Contents
- When to Choose a Time-Series DB Over a Relational DB
- InfluxDB Data Model
- Schema Design for Time-Series
- Flux Query Language Fundamentals
- InfluxQL vs Flux vs SQL
- Downsampling and Continuous Queries / Tasks
- Data Lifecycle Management
- Ingest Patterns
- Integration with Data Engineering Pipelines
- Comparison: InfluxDB vs TimescaleDB vs Prometheus vs QuestDB
1. When to Choose a Time-Series DB Over a Relational DB
Time-Series Data Characteristics
Not all timestamped data is time-series data. True time-series data has these properties:
- Append-heavy: New data points arrive continuously; updates/upserts are rare.
- Time-ordered: Write order closely follows the timestamp order (recent data is hot).
- Time-centric queries: Analysts almost always filter, aggregate, and slice by time ranges.
- Downsampling pattern: Old data is routinely summarized into lower-resolution rollups.
- Immutable by nature: Historical records are almost never modified.
Decision Matrix
| Factor | Choose Relational (PostgreSQL/MySQL) | Choose Time-Series DB (InfluxDB/TimescaleDB) |
|---|---|---|
| Write pattern | Mixed read-write, UPDATE-heavy | Append-only streaming writes |
| Data volume | Millions of rows | Billions to trillions of data points |
| Query pattern | OLTP: single-row lookups, JOINs, transactions | Time-bucket aggregates, range scans |
| Retention | Keep everything indefinitely, DELETE rare | Auto-expire raw data after N days |
| Schema | Frequently evolving, normalized | Stable, denormalized per measurement |
| Consistency | ACID strongly required | Eventual or tunable consistency acceptable |
| Cardinality | Low (user IDs, order IDs) | Can range from low to very high (device IDs, container IDs) |
When to Use a General-Purpose TSDB (like InfluxDB)
- Metrics infrastructure: Server/container CPU, memory, disk, network (DevOps/SRE).
- IoT/IIoT sensor data: Temperature, pressure, vibration readings at high frequency.
- Application telemetry: Request latencies, error rates, user counts.
- Industrial/historian workloads: Replacing PI System, OSIsoft, or other historians.
- Financial tick data: Stock trades, order book snapshots (though QuestDB may be better here).
When to Stick with a Relational DB + Time-Series Extension
- You already have a PostgreSQL ecosystem and want to avoid another infrastructure stack.
- Your time-series data has complex relational joins (e.g., sensor metadata normalized across 5 tables).
- You need full SQL with window functions, CTEs, and transactional guarantees.
- Solution: TimescaleDB hypertables on PostgreSQL.
When to Use an Analytical Columnar DB (ClickHouse) Instead
- You run large ad-hoc analytical queries on time-series data (OLAP-style).
- You need sub-second aggregation over billions of rows across many dimensions.
- Your query patterns are more "GROUP BY time, dimension" than "latest value per series."
2. InfluxDB Data Model
InfluxDB v1 and v2 share a four-component data model. InfluxDB 3 (the current recommended version) retains compatibility with this model but adds SQL-on-Parquet support.
The Four Components
Measurement -> Logical table name (e.g., "cpu", "sensor_temp")
Tag set -> Indexed metadata key=value pairs (e.g., host=server01, region=us-east)
Field set -> Actual data values (e.g., temperature=98.6, cpu_usage=0.85)
Timestamp -> Nanosecond-precision Unix timestamp
Line Protocol Format
This is the canonical way to write data into InfluxDB (all versions):
<measurement>[,<tag_key>=<tag_value>[,<tag_key>=<tag_value>...]] <field_key>=<field_value>[,<field_key>=<field_value>...] [<timestamp>]
Concrete example:
sensor_temp,host=server01,region=us-east temperature=98.6,humidity=0.45 1717610400000000000
Or in InfluxDB 3 SQL terms, the CREATE TABLE equivalent would be:
-- InfluxDB 3 uses SQL for schema management
CREATE TABLE sensor_temp (
time TIMESTAMP,
host STRING, -- tag
region STRING, -- tag
temperature DOUBLE, -- field
humidity DOUBLE -- field
);
How InfluxDB Stores Data
- Tags are indexed — they form the series key. Every unique combination of measurement + tag set defines a time series.
- Fields are not indexed — querying by field values requires a full scan.
- Timestamp is the primary sort key within each series.
- In InfluxDB 3 (IOx engine), data is stored in Parquet files on object storage, with an in-memory catalog for indexing.
- In InfluxDB 1.x/2.x (TSM engine), data is stored in TSM (Time-Structured Merge Tree) files with an in-memory index.
Series Cardinality
series cardinality = number of unique (measurement, tag set) combinations
Example: If you have measurement cpu with tags host (1000 values) and region (5 values), you have up to 5,000 series.
High cardinality is the #1 performance killer in InfluxDB v1/v2 (TSM engine). InfluxDB 3 (IOx) largely solves this by using a columnar storage engine, but high cardinality still affects memory for the catalog.
Values that cause high cardinality and should NEVER be tags:
- Request IDs
- Session IDs
- User IDs (if unique per user and high volume)
- Timestamps as strings
- Email addresses
- Any value with millions of unique values
3. Schema Design for Time-Series
Measurement Design
Rule of thumb: One measurement per logical data source type.
GOOD: measurement="cpu" fields={usage_user, usage_system, usage_idle}
GOOD: measurement="memory" fields={used_bytes, free_bytes, total_bytes}
BAD: measurement="metrics" fields={cpu_user, cpu_system, mem_used, mem_free, disk_read, disk_write}
Tag vs Field Decision Guide
| Put in TAGS if... | Put in FIELDS if... |
|---|---|
| Low to moderate cardinality (< 100K unique values) | The actual measured numeric value |
| You filter or GROUP BY this attribute | High cardinality (request IDs, UUIDs) |
| It's static/reusable metadata (host, region, data_center) | It's the payload/metric value itself |
| You need fast indexed lookups | It changes on every data point |
Tag Cardinality Management
For InfluxDB v1/v2 (TSM):
- Keep total series cardinality under 10 million per node (hard limit ~10-20M).
- Keep per-measurement cardinality under 1 million for good performance.
- Use TSI (Time Series Index) for higher cardinality in v1.7+ but expect memory pressure.
For InfluxDB 3 (IOx/columnar):
- Catalog memory scales with number of unique tag values, not combinations.
- Can handle 100M+ unique series; watch catalog memory (~2-4 GB per 100M series).
Anti-patterns to avoid:
- Putting timestamps, dates, or high-entropy strings as tags.
- Using tags for values that change on every write.
- Over-tagging (10+ tags per measurement when 3-4 would suffice).
- Tag values that grow unboundedly (e.g., container IDs in Kubernetes).
Retention Policies (v1) vs Buckets (v2)
InfluxDB v1 — Retention Policies (RPs):
-- Create a retention policy: keep data for 30 days, 1 replica
CREATE RETENTION POLICY "thirty_days" ON "mydb" DURATION 30d REPLICATION 1 DEFAULT;
InfluxDB v2 — Buckets:
# A bucket combines a database + retention policy from v1
influx bucket create --name "sensor_data_30d" --retention 30d
InfluxDB 3 — Retention at Database Level:
# InfluxDB 3 Core: retention set at database level
# Core OSS enforces a 72-hour default; Cloud Dedicated and Enterprise allow custom retention
influxdb3 create database iot_sensors_prod --retention-period 90d
Best practices:
- Use separate buckets for different retention durations.
- For long-term storage, downsample raw data into a separate measurement with longer retention.
- In InfluxDB 3, the retention period is set per database and defines how long data is kept before automatic deletion.
4. Flux Query Language Fundamentals
Note: Flux was introduced with InfluxDB v2.x. InfluxDB 3 now recommends SQL as the primary query language. Flux is still supported in InfluxDB 2.x and for backward compatibility, but new development on InfluxDB 3 should favor SQL. This section is retained for teams maintaining v2.x workloads.
Basic Structure
Flux is a functional, piped-data language. Every query is a chain of transformations with data flowing left-to-right through pipes (|>).
data_source
|> transformation_1()
|> transformation_2()
|> transformation_3()
Core Functions
// 1. Define the data source and time range
from(bucket: "sensor_data")
|> range(start: -1h) // last hour of data
|> filter(fn: (r) => r._measurement == "cpu")
|> filter(fn: (r) => r._field == "usage_user")
|> filter(fn: (r) => r.host == "server01")
|> yield(name: "cpu_usage")
Common Flux Patterns
Aggregation with windowing (downsampling):
from(bucket: "sensor_data")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "sensor_temp")
|> aggregateWindow(every: 1h, fn: mean)
|> yield(name: "hourly_mean")
Multiple aggregations in one query:
from(bucket: "sensor_data")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_user")
|> aggregateWindow(every: 15m, fn: mean)
|> duplicate(column: "_stop", as: "_time")
|> drop(columns: ["_start", "_stop"])
|> set(key: "_field", value: "usage_user_mean")
|> to(bucket: "downsampled_cpu")
Pivoting to wide format (useful for Grafana):
from(bucket: "sensor_data")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|> yield(name: "wide")
Joining data streams:
cpu = from(bucket: "sensor_data")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_user")
mem = from(bucket: "sensor_data")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "mem" and r._field == "used_percent")
join(tables: {cpu: cpu, mem: mem}, on: ["_time", "host"])
|> yield(name: "joined")
Flux Task (recurring script)
// Run every hour
option task = {
name: "downsample_cpu_hourly",
every: 1h,
offset: 5m
}
from(bucket: "raw_sensor_data")
|> range(start: -2h)
|> filter(fn: (r) => r._measurement == "cpu")
|> aggregateWindow(every: 1h, fn: mean)
|> to(bucket: "downsampled_cpu")
5. InfluxQL vs Flux vs SQL
Overview
| Feature | InfluxQL | Flux | SQL (InfluxDB 3) |
|---|---|---|---|
| Era | InfluxDB 1.x | InfluxDB 2.x | InfluxDB 3 (current) |
| Style | SQL-like | Functional / piped | Standard SQL |
| Complexity | Low | Medium-High | Low |
| Learning curve | Easy (if you know SQL) | Steep | Easy (if you know SQL) |
| Multi-bucket queries | No | Yes | Yes |
| Joins | Limited (subqueries only) | Native | Full SQL JOINs |
| Scripting | No | Yes (variables, conditionals, functions) | Via SQL functions |
| Window functions | Limited | Native | Yes |
| Performance | Good | Medium (interpreted) | Best (compiled) |
| Status in InfluxDB 3 | Read-only compatibility | Supported for compatibility | Recommended |
When to Use Which
Use SQL (InfluxDB 3): Default for ALL new projects on InfluxDB 3.
Use Flux (v2.x only): Existing v2.x deployments, complex transformation pipelines.
Use InfluxQL (v1.x): Existing v1.x deployments, minimal migration path.
InfluxQL vs Flux: Equivalent Queries
InfluxQL:
SELECT mean("usage_user")
FROM "cpu"
WHERE time > now() - 1h
AND "host" = 'server01'
GROUP BY time(15m)
Flux:
from(bucket: "mydb/autogen")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_user" and r.host == "server01")
|> aggregateWindow(every: 15m, fn: mean)
SQL (InfluxDB 3):
SELECT DATE_BIN(INTERVAL '15 minutes', time) AS bucket,
AVG(usage_user) AS avg_usage
FROM cpu
WHERE time > now() - INTERVAL '1 hour'
AND host = 'server01'
GROUP BY bucket
ORDER BY bucket;
Key Migration Notes
- InfluxQL
GROUP BY time(interval)→ FluxaggregateWindow(every: interval, fn: ...) - InfluxQL
INTO(downsample+write) → Flux... |> to(bucket: "...") - Continuous Queries (InfluxQL) → Tasks (Flux)
- InfluxDB 3: Rewrite InfluxQL CQs as SQL scheduled queries or use external orchestrators.
6. Downsampling and Continuous Queries / Tasks
The Downsampling Pattern
Downsampling is the most critical data engineering pattern for time-series:
RAW DATA (1-second resolution, keep 30 days)
|
v [downsample every hour]
HOURLY ROLLUPS (1-minute aggregates, keep 1 year)
|
v [downsample every day]
DAILY ROLLUPS (1-hour aggregates, keep 5 years)
Each tier provides exponentially smaller storage footprint while preserving analytical value.
InfluxDB 1.x: Continuous Queries (CQs)
CREATE CONTINUOUS QUERY "cq_cpu_hourly" ON "mydb"
BEGIN
SELECT mean("usage_user") AS "mean_usage"
INTO "mydb"."downsampled"."cpu_hourly"
FROM "cpu"
GROUP BY time(1h), "host"
END;
CQs run automatically at the end of each time window. They are simple but limited: only one aggregation function per CQ, no chaining.
InfluxDB 2.x: Tasks (Flux-based)
// downsample_cpu_hourly
option task = {
name: "downsample_cpu_hourly",
every: 1h,
offset: 10m
}
from(bucket: "raw_data")
|> range(start: -task.every)
|> filter(fn: (r) => r._measurement == "cpu")
|> aggregateWindow(every: 1h, fn: mean)
|> set(key: "_measurement", value: "cpu_hourly")
|> to(bucket: "downsampled_data")
Chained (hierarchical) downsampling with tasks:
Task 1: raw_1s -> hourly (runs every hour)
Task 2: hourly -> daily (runs every day, queries the hourly bucket)
Task 3: daily -> monthly (runs monthly, queries the daily bucket)
InfluxDB 3: Scheduled Queries / External Orchestration
InfluxDB 3 Core does not have built-in continuous aggregates. Recommended approaches:
-
External scheduler (Airflow, cron, Prefect):
-- Run hourly via Airflow INSERT INTO cpu_hourly SELECT DATE_BIN(INTERVAL '1 hour', time) AS bucket, host, AVG(usage_user) AS avg_usage, MIN(usage_user) AS min_usage, MAX(usage_user) AS max_usage, COUNT(*) AS sample_count FROM cpu WHERE time > now() - INTERVAL '2 hours' GROUP BY bucket, host; -
InfluxDB 3 processing engine plugins for real-time transformations on write.
-
Write-time processing via Telegraf aggregator plugins:
[[processors.aggregate]] period = "60s" [[processors.aggregate.config]] measurement = "cpu" columns = ["usage_user", "usage_system"] functions = ["mean", "max", "min"]
Downsampling Best Practices
- Store raw data at full resolution for the shortest practical window.
- Always include
COUNT(*)in aggregates to track sample density. - Use hierarchical aggregation (aggregate hourly data into daily, not raw into daily).
- Align your downsampling schedule with your retention policies.
- Consider two-phase downsampling: real-time via Telegraf aggregators for the first 1-5 minutes, then batch tasks for correction/backfill.
7. Data Lifecycle Management
Storage Engine Architecture
InfluxDB 1.x / 2.x (TSM Engine):
- Data organized into shards by time range (typically 7 days).
- Each shard is a set of TSM files + Write-Ahead Log (WAL).
- Compaction merges smaller TSM files into larger ones, removing deleted/overwritten data.
- Shard management: Default 7-day shard duration; configurable.
- Memory index (in-memory) maps series keys to TSM file locations.
InfluxDB 3 (IOx Engine):
- Data stored as Parquet files in object storage (S3, local FS).
- Catalog (SQLite or PostgreSQL) tracks table/column metadata.
- Compaction merges Parquet files for read efficiency.
- No shards per se — data is partitioned by time but managed at the file level.
Retention Lifecycle Strategy
Example: 3-tier retention for IoT sensor data
Tier 1: Raw (1-second resolution) -> 30 days -> bucket "raw_30d"
Tier 2: Hourly aggregates -> 1 year -> bucket "hourly_1y"
Tier 3: Daily aggregates -> 5 years -> bucket "daily_5y"
InfluxDB v2/v3 implementation:
- Create three buckets with different retention periods.
- Run downsampling tasks from raw -> hourly -> daily.
- InfluxDB automatically deletes data older than each bucket's retention period.
Compaction
TSM compaction stages:
- Level 1 (snapshot): WAL -> TSM file (when WAL reaches threshold).
- Level 2 (merge): 2-4 small TSM files -> 1 larger TSM file.
- Level 3 (full): Multiple TSM files -> 1 optimized TSM file (deduplicates, removes tombstones).
- Compaction runs automatically; tune
cache-snapshot-write-cold-durationandcompact-full-write-cold-durationfor write-heavy workloads.
IOx/Parquet compaction:
- Merges small Parquet files (< 100 MB) into larger ones (~100-500 MB).
- Runs automatically but can be triggered manually via API.
- Compaction also applies retention deletion.
Shard Management (InfluxDB v1/v2)
Command: Effect:
ALTER RETENTION Change shard duration (default 7d)
POLICY ... DURATION
DROP SHARD Force-delete a specific shard and all its data
SHOW SHARDS List all shards with durations, sizes, status
influx_inspect Low-level TSM inspection and recovery tools
Choosing shard duration:
- Short shard duration (1-7d): More granular retention, easier to drop old data, more overhead.
- Long shard duration (1-4w): Less overhead, faster range queries, slower retention enforcement.
- Rule: shard duration should be ≤ 1/2 of your retention period for efficient expiry.
Cold / Tiered Storage
- InfluxDB Cloud Serverless: Automatically tiers data to object storage.
- InfluxDB Cloud Dedicated: Configurable cold storage with Parquet.
- TimescaleDB: Native tiering to S3 via
tieringpolicies. - InfluxDB OSS: No built-in tiering; manage via external scripts or data migration.
8. Ingest Patterns
Line Protocol
The core ingestion format for all InfluxDB versions.
Format:
measurement,tag1=val1,tag2=val2 field1=val1,field2=val2 timestamp
Data types:
- Tags: strings only (no quoting needed if no special chars).
- Fields: floats (default), integers (trailing
i), strings ("quoted"), booleans (t/f/true/false). - Timestamp: nanosecond epoch (default); configurable precision (s, ms, us, ns).
Examples:
# Float fields (default)
weather,location=us-midwest temperature=82.0 1465839830100400200
# Integer field (trailing i)
weather,location=us-midwest wind_speed=15i 1465839830100400200
# String field (double-quoted)
weather,location=us-midwest conditions="partly cloudy" 1465839830100400200
# Boolean field
weather,location=us-midwest is_raining=t 1465839830100400200
# Multiple fields
weather,location=us-midwest temperature=82.0,humidity=71.2 1465839830100400200
Write via HTTP API:
curl -X POST \
"http://localhost:8086/write?db=mydb&precision=s" \
--data-raw "weather,location=us-midwest temperature=82.0 1465839830"
Batch vs Streaming Writes
| Factor | Batch | Streaming |
|---|---|---|
| Frequency | Every N seconds or N points | Every point as it arrives |
| Overhead | Low (HTTP overhead amortized) | High (per-request overhead) |
| Throughput | High (10K-100K points/s per node) | Low (1K-10K points/s per node) |
| Latency | Seconds to minutes | Sub-second |
| Use case | Backfill, batch ETL | Real-time monitoring, Telegraf |
Best practice: Always batch writes — send 1,000-10,000 points per HTTP request. Never send single points.
Telegraf Agent
Telegraf is InfluxData's plugin-driven collection agent.
Architecture:
Input Plugins -> Aggregator/Processor Plugins -> Output Plugins
| | |
(CPU, disk, (aggregate, (InfluxDB,
MQTT, Kafka, transform, Prometheus,
Prometheus, enrich, file, Kafka,
syslog, SNMP, filter) CloudWatch)
Docker, k8s...)
Example Telegraf config (telegraf.conf):
# Global settings
[agent]
interval = "10s"
flush_interval = "10s"
metric_batch_size = 5000
# Input: CPU metrics
[[inputs.cpu]]
percpu = true
totalcpu = true
# Input: MQTT subscriber
[[inputs.mqtt_consumer]]
servers = ["tcp://broker.local:1883"]
topics = ["sensors/#"]
data_format = "json"
json_time_key = "timestamp"
json_time_format = "unix_ms"
tag_keys = ["device_id", "location"]
# Processor: apply transformation
[[processors.enum]]
[[processors.enum.mapping]]
tag = "status"
value_mappings = {online = 1, offline = 0}
# Aggregator: downsample in real-time
[[processors.aggregate]]
period = "60s"
[[processors.aggregate.config]]
measurement = "cpu"
columns = ["usage_idle", "usage_user"]
functions = ["mean", "min", "max"]
# Output: InfluxDB v2
[[outputs.influxdb_v2]]
urls = ["http://localhost:8086"]
token = "${INFLUX_TOKEN}"
organization = "myorg"
bucket = "sensor_data"
# Output: backup to file (for audit trail)
[[outputs.file]]
files = ["/var/log/telegraf_audit.log"]
data_format = "json"
Telegraf best practices:
- Use
metric_batch_size(5,000-10,000) andflush_interval(5-10s) for efficient batching. - Tag inputs with consistent metadata (data center, region, environment).
- Use processor and aggregator plugins rather than sending raw data and downsampling later.
- Set
fieldpass/fielddropon inputs to avoid collecting unused metrics. - File-based logging output for auditability and data recovery.
Ingest Performance Tuning
| Parameter | InfluxDB v1/v2 (TSM) | InfluxDB 3 (IOx) |
|---|---|---|
| Write batch size | 5,000-10,000 points | 10,000-50,000 points |
| HTTP workers | 8-16 | 16-64 |
| WAL flush interval | 1-10s (lower = safer) | N/A (no WAL) |
| Max points per second (single node) | 500K-1M | 3M-10M |
| Bottleneck | CPU (index updates) | Network I/O (Parquet writes) |
Kafka Integration
Telegraf as Kafka consumer:
[[inputs.kafka_consumer]]
brokers = ["kafka:9092"]
topics = ["sensor_events"]
group_id = "telegraf_ingest"
data_format = "json"
consumer_fetch_min = "100KB"
consumer_fetch_default = "1MB"
max_undelivered_messages = 10000
Telegraf as Kafka producer:
[[outputs.kafka]]
brokers = ["kafka:9092"]
topic = "aggregated_metrics"
data_format = "json"
compression_codec = 2 # snappy
required_acks = -1 # all
9. Integration with Data Engineering Pipelines
Apache Airflow DAG Example
from airflow import DAG
from airflow.providers.http.operators.http import HttpOperator
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'data_engineering',
'retries': 2,
'retry_delay': timedelta(minutes=5),
}
dag = DAG(
'influxdb_downsample_hourly',
schedule='0 * * * *', # Every hour
start_date=datetime(2025, 1, 1),
catchup=False,
)
def generate_line_protocol(**context):
"""Generate downsampled data from raw and write hourly rollup."""
import requests
import json
# Query raw hourly aggregates via SQL (InfluxDB 3)
raw_data = requests.get(
f"{INFLUX_HOST}/api/v3/query",
params={"db": "sensor_raw"},
headers={"Authorization": f"Bearer {INFLUX_TOKEN}"},
data={
"query": """
SELECT DATE_BIN(INTERVAL '1 hour', time) AS bucket,
host, region, AVG(temperature) AS avg_temp,
MIN(temperature) AS min_temp, MAX(temperature) AS max_temp,
COUNT(*) AS sample_count
FROM sensor_readings
WHERE time >= now() - INTERVAL '2 hours'
AND time < now() - INTERVAL '1 hour'
GROUP BY bucket, host, region
"""
}
)
# Convert to line protocol and write to hourly bucket
points = []
for row in raw_data.json():
lp = (
f"sensor_hourly,host={row['host']},region={row['region']} "
f"avg_temp={row['avg_temp']},min_temp={row['min_temp']},"
f"max_temp={row['max_temp']},sample_count={row['sample_count']}i "
f"{row['bucket']}"
)
points.append(lp)
# Batch write
requests.post(
f"{INFLUX_HOST}/api/v2/write",
params={"bucket": "sensor_hourly", "precision": "ms"},
headers={"Authorization": f"Bearer {INFLUX_TOKEN}"},
data="\n".join(points),
)
Apache Spark / PySpark Integration
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("influxdb_etl").getOrCreate()
# Read from InfluxDB v3 using JDBC (PostgreSQL-compatible driver)
df = spark.read \
.format("jdbc") \
.option("url", f"jdbc:postgresql://{INFLUX_HOST}:5432/mydb") \
.option("query", """
SELECT time, host, region, temperature
FROM sensor_readings
WHERE time >= now() - INTERVAL '24 hours'
""") \
.option("user", INFLUX_USER) \
.option("password", INFLUX_PASS) \
.option("driver", "org.postgresql.Driver") \
.load()
# Transform
hourly_agg = df.groupBy(
F.window("time", "1 hour").alias("bucket"),
"host", "region"
).agg(
F.avg("temperature").alias("avg_temp"),
F.min("temperature").alias("min_temp"),
F.max("temperature").alias("max_temp"),
F.count("*").alias("sample_count")
)
# Write back to InfluxDB
hourly_agg.write \
.format("jdbc") \
.option("url", f"jdbc:postgresql://{INFLUX_HOST}:5432/downsampled_db") \
.option("dbtable", "sensor_hourly") \
.option("user", INFLUX_USER) \
.option("password", INFLUX_PASS) \
.mode("append") \
.save()
Grafana Integration
Grafana is the most common visualization layer for InfluxDB:
Grafana Data Source:
Type: InfluxDB
URL: http://influxdb:8086
Database: mydb (v1) / Organization: myorg, Bucket: sensor_data (v2/v3)
Min time interval: 10s (matches collection interval)
Version: InfluxQL (v1) / Flux (v2) / SQL (v3)
ETL Pipeline Patterns
Pattern 1: Telegraf -> InfluxDB -> Grafana
(simplest, real-time monitoring)
Pattern 2: Sensors -> MQTT/Kafka -> Telegraf -> InfluxDB -> Grafana
(Kafka for buffering, backpressure handling)
Pattern 3: Sensors -> Kafka -> Flink/Spark -> InfluxDB -> Airflow (downsample) -> InfluxDB
(heavy stream processing + scheduled downsampling)
Pattern 4: Application -> InfluxDB -> Airflow/Spark -> Parquet -> S3/Data Lake
(time-series data lake architecture)
10. Comparison: InfluxDB vs TimescaleDB vs Prometheus vs QuestDB
At a Glance
| Feature | InfluxDB 3 | TimescaleDB | Prometheus | QuestDB |
|---|---|---|---|---|
| Type | Purpose-built TSDB | PostgreSQL extension | Monitoring + TSDB | Purpose-built TSDB |
| Engine | Columnar (Parquet/IOx) | Hybrid row-columnar (Hypercore) | Custom TSDB engine | Columnar (custom) |
| Query Language | SQL + Flux + InfluxQL | Full SQL + PG extensions | PromQL | SQL |
| Ingest Protocol | Line Protocol, SQL, InfluxDB v2 API | PostgreSQL INSERT, COPY | Push via remote write | Line Protocol, InfluxDB, PostgreSQL wire |
| Storage | Parquet files on object storage | PostgreSQL on local/cloud disk | Local TSDB blocks | Memory-mapped files + disk |
| Compression | Good (Parquet) | Excellent (up to 95%) | Good (snappy) | Good |
| High Availability | Enterprise / Cloud (multi-node) | Streaming replication (PG built-in) | Sidecar (Thanos/Cortex) | Enterprise (pending) |
| Clustering | Enterprise only | PG-based | Built-in with Thanos | Enterprise |
| Retention | Per-database config | Per-hypertable via policies | Configurable (local) | Partition-based |
| Continuous Aggregation | External only (Core) | Built-in (continuous aggregates) | Recording rules | Materialized views |
| Downsampling | Tasks / external orchestration | Continuous aggregates | Recording rules + federation | SAMPLE BY + scheduled queries |
| Real-time performance | Excellent (3M+ points/s per node) | Very good (1M+ points/s) | Good (1M samples/s) | Excellent (5M+ points/s) |
| SQL compatibility | High (PostgreSQL-like) | Complete (PostgreSQL) | None (PromQL only) | High (custom SQL) |
| Data lake export | Native Parquet output | Via PG tools | Remote write / Thanos | Native Parquet output |
| Best for... | General TSDB, IoT, app metrics, edge | Teams already on PostgreSQL, need full SQL | Kubernetes monitoring, site reliability | Lowest-latency, financial tick data, HFT |
Detailed Comparison
InfluxDB 3
- Strengths: Mature ecosystem (Telegraf, 300+ plugins), multiple deployment options (edge to cloud), native line protocol (de facto standard), good for heterogeneous data sources.
- Weaknesses: Flux deprecation path creates migration friction; Core OSS has 72h retention limit; no built-in continuous aggregates in Core; clustering only in Enterprise.
- Best fit: General-purpose time-series, DevOps/SRE monitoring, IoT/IIoT, replacing legacy historians.
TimescaleDB (Tiger Data)
- Strengths: Full PostgreSQL compatibility (all SQL, all PG extensions, all tools), continuous aggregates with incremental refresh, hierarchical aggregation, excellent compression (up to 95%), hypertables with automatic partitioning, mature replication/PITR/HA from PostgreSQL.
- Weaknesses: Slightly lower raw ingest throughput than InfluxDB 3 or QuestDB; requires PostgreSQL knowledge; not as lightweight for edge deployments; parent company rebranded to Tiger Data (some confusion).
- Best fit: Data teams already on PostgreSQL; workloads needing complex JOINs, transactions, or full SQL analytics; long-term historical storage.
Prometheus
- Strengths: Cloud-native standard for Kubernetes monitoring, simple operational model (single binary), PromQL is excellent for alerting and service-level metrics, pull-based model works well for dynamic infra.
- Weaknesses: Not a general-purpose TSDB (no SQL, no complex aggregations), limited retention (default 15d), single-node, no native HA (requires Thanos/Cortex), poor for IoT or high-cardinality label sets.
- Best fit: Kubernetes and container monitoring, service-level dashboards, alerting (PagerDuty/AlertManager), infra metrics.
QuestDB
- Strengths: Highest raw ingest throughput (claimed 5M+ points/s on single node), lowest query latency for time-bucket aggregations, designed for capital markets/finance, native InfluxDB line protocol and PostgreSQL wire protocol support, SQL-compatible, non-blocking ingestion (immutable append), parallelized and vectorized query execution.
- Weaknesses: Smaller ecosystem (fewer integrations, fewer client libraries), newer project (less mature), clustering is Enterprise-only, less tooling for alerting/monitoring out of the box.
- Best fit: Financial tick data, high-frequency trading, real-time dashboards demanding microsecond query latency, capital markets infrastructure.
Decision Flowchart
Q: What infrastructure are you already running?
|
+-- PostgreSQL everywhere?
| |--> TimescaleDB (stay in PG ecosystem)
|
+-- Kubernetes / containers?
| |--> Prometheus for infra monitoring
| +--> InfluxDB for app metrics and IoT
|
+-- Need lowest possible latency (< 1ms queries)?
| |--> QuestDB (financial, HFT)
|
+-- Heterogeneous environment, edge devices, IoT?
|--> InfluxDB (Telegraf ecosystem, multiple deployment options)
Q: What query language do you need?
|--> Full SQL with JOINs, CTEs, window functions? -> TimescaleDB or InfluxDB 3
|--> Time-series specific: PromQL? -> Prometheus
|--> Time-series specific: Flux? -> InfluxDB 2.x
|--> DevOps dashboards: Grafana + search? -> Any (Grafana supports all)
Q: How much data are you ingesting?
|--> < 100K points/s -> Any
|--> 100K-1M points/s -> InfluxDB or TimescaleDB
|--> > 1M points/s -> QuestDB or InfluxDB 3
|--> > 5M points/s -> QuestDB (benchmark leader)
Version Guidance (InfluxDB Specific)
| Deployment | Best For | Query Language |
|---|---|---|
| InfluxDB 3 Cloud Serverless | Rapid prototyping, variable workloads | SQL (recommended) |
| InfluxDB 3 Cloud Dedicated | Predictable production workloads | SQL (recommended) |
| InfluxDB 3 Enterprise | Self-managed HA production | SQL (recommended) |
| InfluxDB 3 Core | Edge, dev, prototypes | SQL (recommended) |
| InfluxDB OSS v2 | Existing v2.x deployments | Flux (migrate to SQL when moving to v3) |
| InfluxDB OSS v1 | Legacy, no migration budget yet | InfluxQL (migrate to SQL when possible) |
Appendix: Quick Reference
Line Protocol Cheatsheet
# Measurement name
weather
# Tags (comma-separated after measurement, space before fields)
weather,location=us-midwest,station=A
# Fields (comma-separated, space before timestamp)
weather temperature=82.0,humidity=71.2
# Timestamp (nanoseconds since epoch; space after fields)
weather temperature=82.0 1465839830100400200
# Data type suffixes:
# Integer: value=42i
# Float: value=3.14 (default)
# String: value="hello world"
# Boolean: value=t (t/true/True/TRUE)
# value=f (f/false/False/FALSE)
# Timestamp: value=1465839830100400200 (nanosecond)
# Escaping:
# Commas in tag values: tag=hello\,world
# Spaces in tag values: tag=hello\ world
# Equals in tag values: tag=hello\=world
# Double-quotes in string fields: field="say \"hello\""
# Backslashes: tag=path\\to\\dir
Key InfluxDB v2/v3 CLI Commands
# InfluxDB v2
influx bucket create --name my_bucket --retention 30d
influx task create --file downsample.flux
influx query --file my_query.flux
# InfluxDB 3 Core
influxdb3 create database my_db --retention-period 90d
influxdb3 write --db my_db --file data.lp
influxdb3 query --db my_db "SELECT * FROM cpu WHERE time > now() - INTERVAL '1 hour'"
Key TimescaleDB SQL Commands
-- Create hypertable
SELECT create_hypertable('sensor_readings', 'time');
-- Add compression
ALTER TABLE sensor_readings SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id'
);
SELECT add_compression_policy('sensor_readings', INTERVAL '7 days');
-- Continuous aggregate
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(value) AS avg_value
FROM sensor_readings
GROUP BY bucket, sensor_id;
Generated: 2025-06-05 | InfluxDB 3 (IOx/columnar engine) is the current recommended version. Flux is supported for v2.x compatibility; new projects should prefer SQL.