Time Series Database for IoT: QuestDB vs InfluxDB vs TDengine 2026

数据库

Time Series Database for IoT: QuestDB vs InfluxDB vs TDengine

In the IoT landscape of 2026, your sensors spit out tens of thousands of data points per second — temperature, humidity, vibration frequency, GPS coordinates... Traditional MySQL? Write bottleneck crashes it in 5 minutes. MongoDB? Time-series aggregation queries are so slow you question reality.

Choose the wrong time series database, and you'll face query timeouts, storage explosions, or even total data pipeline collapse. QuestDB, InfluxDB, and TDengine each have their strengths — but which one is the optimal choice for your IoT scenario? This article walks through 5 core production patterns to help you decide definitively.

Core Concepts at a Glance

Concept Description Typical Scenario
Time Series Chronologically ordered data point sequence Sensor collection, monitoring metrics
Tag Dimension/index field of data Device ID, region, type
Field Measured value of data Temperature value, voltage value
Downsampling Aggregating high-frequency data to low-frequency Second-level → minute-level → hour-level
Retention Policy Automatic data expiration and cleanup Hot data 7 days, cold data 1 year
Super Table TDengine-specific, template for multi-device tables 1000 devices sharing one template

5 Major Pain Points of IoT Time Series Data

  1. Write throughput bottleneck: Million-level concurrent device writes, traditional databases can't handle the IOPS
  2. Inefficient query aggregation: Time window aggregation and downsampling queries have poor performance, P99 latency spikes
  3. Storage cost out of control: Large and continuously growing time series data, difficult hot/cold data separation
  4. Complex multi-device management: Thousands of devices each with tags, schema management chaos
  5. Fragmented ecosystem integration: High integration cost with Grafana, Kafka, Telegraf and other toolchains

Pattern 1: IoT Time Series Data Characteristics and Modeling

IoT time series data has distinct characteristics: high-cardinality tags, continuous append-only writes, strong time locality, read-heavy and write-heavy. Understanding these characteristics is the prerequisite for selection.

# Python: Simulate IoT time series data generation
# Environment: Python 3.12+ / No extra dependencies
import time
import json
import random
from datetime import datetime, timezone

def generate_iot_sensor_data(device_count: int = 100, interval_ms: int = 1000) -> dict:
    """Generate IoT sensor time series data point
    
    Args:
        device_count: Number of devices
        interval_ms: Collection interval (milliseconds)
    
    Returns:
        Single time series data point
    """
    device_id = f"sensor-{random.randint(1, device_count):04d}"
    region = random.choice(["us-east", "us-west", "eu-central", "ap-southeast"])
    sensor_type = random.choice(["temperature", "humidity", "vibration", "pressure"])
    
    base_values = {
        "temperature": 25.0,
        "humidity": 60.0,
        "vibration": 0.5,
        "pressure": 101.3,
    }
    value = round(base_values[sensor_type] + random.gauss(0, 2), 3)
    
    return {
        "timestamp": int(time.time() * 1_000_000),  # Microsecond precision
        "tags": {
            "device_id": device_id,
            "region": region,
            "sensor_type": sensor_type,
            "factory": f"plant-{region.split('-')[0]}",
        },
        "fields": {
            "value": value,
            "quality": random.choice(["good", "good", "good", "warning", "error"]),
            "battery": round(random.uniform(10, 100), 1),
        },
    }


def simulate_iot_stream(duration_seconds: int = 10, devices: int = 100) -> list[dict]:
    """Simulate IoT data stream
    
    Args:
        duration_seconds: Simulation duration (seconds)
        devices: Number of devices
    
    Returns:
        List of time series data points
    """
    data_points = []
    start = time.time()
    
    while time.time() - start < duration_seconds:
        point = generate_iot_sensor_data(device_count=devices)
        data_points.append(point)
        time.sleep(0.01)  # Simulate 100Hz collection
    
    print(f"Generated {len(data_points)} data points, "
          f"duration {duration_seconds}s, "
          f"devices {devices}")
    return data_points


if __name__ == "__main__":
    points = simulate_iot_stream(duration_seconds=5, devices=50)
    print(json.dumps(points[0], indent=2))

Pattern 2: QuestDB SQL Time Series Queries

QuestDB is known for its "SQL-first" approach, with time series extensions to standard SQL that make queries extremely intuitive. Zero-dependency installation and high-performance writes are its killer features.

-- QuestDB: IoT time series SQL query patterns
-- Environment: QuestDB 8.x / ILP (InfluxDB Line Protocol) writes

-- 1. Create table
CREATE TABLE IF NOT EXISTS sensor_data (
    timestamp TIMESTAMP,
    device_id SYMBOL,
    region SYMBOL,
    sensor_type SYMBOL,
    factory SYMBOL,
    value DOUBLE,
    quality SYMBOL,
    battery DOUBLE
) TIMESTAMP(timestamp) PARTITION BY DAY WAL;

-- 2. Time window aggregation - 5-minute average temperature
SELECT
    timestamp,
    device_id,
    avg(value) AS avg_temp,
    min(value) AS min_temp,
    max(value) AS max_temp,
    count() AS sample_count
FROM sensor_data
WHERE sensor_type = 'temperature'
  AND timestamp >= dateadd('h', -1, now())
SAMPLE BY 5m ALIGN TO CALENDAR;

-- 3. Latest value query - LATEST ON syntax
SELECT * FROM sensor_data LATEST ON timestamp PARTITION BY device_id;

-- 4. Downsampling + multi-dimension grouping
SELECT
    timestamp,
    region,
    sensor_type,
    avg(value) AS avg_value,
    stddev(value) AS std_value,
    count() AS data_points
FROM sensor_data
WHERE timestamp >= dateadd('d', -7, now())
SAMPLE BY 1h ALIGN TO CALENDAR
GROUP BY region, sensor_type;

-- 5. Anomaly detection - 3 consecutive points exceeding threshold
WITH spike_detected AS (
    SELECT
        timestamp,
        device_id,
        value,
        case when value > 35 OR value < 15 THEN 1 ELSE 0 END AS is_spike
    FROM sensor_data
    WHERE sensor_type = 'temperature'
)
SELECT
    timestamp,
    device_id,
    value,
    sum(is_spike) OVER (
        ORDER BY timestamp
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS consecutive_spikes
FROM spike_detected
WHERE consecutive_spikes >= 3;
# Python: QuestDB ILP write client
# Environment: Python 3.12+ / pip install questdb
from questdb.ingress import IngressError, Sender, Buffer, Protocol

def write_iot_data_questdb(host: str = "localhost", port: int = 9009):
    """Write IoT data to QuestDB via ILP protocol"""
    conf = f"http::addr={host}:{port};"
    
    try:
        with Sender.from_conf(conf) as sender:
            for i in range(1000):
                device_id = f"sensor-{i % 50:04d}"
                region = ["us-east", "us-west", "eu-central"][i % 3]
                
                sender.row(
                    "sensor_data",
                    symbols={
                        "device_id": device_id,
                        "region": region,
                        "sensor_type": "temperature",
                        "factory": f"plant-{region.split('-')[0]}",
                    },
                    columns={
                        "value": 25.0 + (i % 10) * 0.5,
                        "battery": 80.0 + (i % 20),
                    },
                    at=Sender.current_timestamp_with_nanos(),
                )
            
            sender.flush()
            print(f"Successfully wrote 1000 data points to QuestDB")
            
    except IngressError as e:
        print(f"Write failed: {e}")


if __name__ == "__main__":
    write_iot_data_questdb()

Pattern 3: InfluxDB Flux Queries

InfluxDB 3.x has moved to SQL queries, but Flux is still widely used in the 2.x ecosystem. Mastering Flux is essential for maintaining existing systems.

# Python: InfluxDB 2.x Flux query patterns
# Environment: Python 3.12+ / pip install influxdb-client[async]
from influxdb_client import InfluxDBClient, Point, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import datetime

INFLUX_URL = "http://localhost:8086"
INFLUX_TOKEN = "your-api-token"
INFLUX_ORG = "iot-org"
INFLUX_BUCKET = "sensor-bucket"


def write_iot_data_influxdb():
    """Write IoT data to InfluxDB 2.x"""
    client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
    write_api = client.write_api(write_options=SYNCHRONOUS)
    
    points = []
    for i in range(500):
        point = (
            Point("sensor_data")
            .tag("device_id", f"sensor-{i % 50:04d}")
            .tag("region", ["us-east", "us-west", "eu-central"][i % 3])
            .tag("sensor_type", "temperature")
            .field("value", 25.0 + (i % 10) * 0.5)
            .field("battery", 80.0 + (i % 20))
            .time(datetime.datetime.utcnow(), write_precision="ms")
        )
        points.append(point)
    
    write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=points)
    print(f"Successfully wrote {len(points)} data points to InfluxDB")
    client.close()


FLUX_QUERIES = """
// 1. Time window aggregation - 5-minute average temperature
from(bucket: "sensor-bucket")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "sensor_data")
  |> filter(fn: (r) => r.sensor_type == "temperature")
  |> filter(fn: (r) => r._field == "value")
  |> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
  |> yield(name: "avg_temp_5m")

// 2. Multi-dimension grouping statistics
from(bucket: "sensor-bucket")
  |> range(start: -7d)
  |> filter(fn: (r) => r._measurement == "sensor_data")
  |> filter(fn: (r) => r._field == "value")
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
  |> group(columns: ["region", "sensor_type"])
  |> yield(name: "hourly_by_region_type")

// 3. Anomaly detection - consecutive threshold exceedances
from(bucket: "sensor-bucket")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "sensor_data")
  |> filter(fn: (r) => r._field == "value")
  |> map(fn: (r) => ({r with is_spike: if r._value > 35.0 or r._value < 15.0 then 1 else 0}))
  |> window(every: 5m)
  |> sum(column: "is_spike")
  |> filter(fn: (r) => r.is_spike >= 3)
  |> yield(name: "spike_alert")

// 4. Downsampling write to continuous query
from(bucket: "sensor-bucket")
  |> range(start: -30d)
  |> filter(fn: (r) => r._measurement == "sensor_data")
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
  |> to(bucket: "sensor-downsampled", org: "iot-org")
"""

if __name__ == "__main__":
    write_iot_data_influxdb()
    print("\nFlux query examples:")
    print(FLUX_QUERIES)

Pattern 4: TDengine Super Tables

TDengine's super table is a killer feature for IoT scenarios: one super table template manages thousands of devices, sub-tables automatically inherit tags, and query performance is extremely high.

-- TDengine 3.x: IoT super table patterns
-- Environment: TDengine 3.3+ / taos CLI

-- 1. Create super table (template)
CREATE STABLE IF NOT EXISTS sensor_data (
    ts TIMESTAMP,
    value FLOAT,
    battery FLOAT,
    quality NCHAR(10)
) TAGS (
    device_id NCHAR(20),
    region NCHAR(20),
    sensor_type NCHAR(20),
    factory NCHAR(20)
);

-- 2. Auto-create sub-table and write
INSERT INTO d_sensor_0001 USING sensor_data TAGS ('sensor-0001', 'us-east', 'temperature', 'plant-us')
VALUES (NOW + 0a, 25.3, 85.2, 'good');

INSERT INTO d_sensor_0002 USING sensor_data TAGS ('sensor-0002', 'us-west', 'humidity', 'plant-us')
VALUES (NOW + 0a, 62.1, 91.5, 'good');

-- 3. Batch write to multiple devices
INSERT INTO 
    d_sensor_0001 VALUES (NOW + 1a, 25.5, 85.0, 'good') (NOW + 2a, 25.7, 84.8, 'good')
    d_sensor_0002 VALUES (NOW + 1a, 62.3, 91.2, 'good') (NOW + 2a, 62.0, 91.0, 'warning');

-- 4. Super table aggregation query - by region
SELECT
    region,
    sensor_type,
    AVG(value) AS avg_value,
    STDDEV(value) AS std_value,
    COUNT(*) AS data_points
FROM sensor_data
WHERE ts >= NOW - 1h
INTERVAL(5m)
GROUP BY region, sensor_type;

-- 5. Latest value query - LAST_ROW
SELECT
    device_id,
    LAST_ROW(value) AS latest_value,
    LAST_ROW(ts) AS last_update
FROM sensor_data
GROUP BY device_id;

-- 6. Downsampling + window function
SELECT
    _wstart AS window_start,
    _wend AS window_end,
    device_id,
    AVG(value) AS avg_value,
    MIN(value) AS min_value,
    MAX(value) AS max_value
FROM sensor_data
WHERE ts >= NOW - 7d AND sensor_type = 'temperature'
INTERVAL(1h) SLIDING(30m)
GROUP BY device_id;
# Python: TDengine write client
# Environment: Python 3.12+ / pip install taos-ws-py
import taosws

def write_iot_data_tdengine(host: str = "localhost", port: int = 6041):
    """Write IoT data to TDengine 3.x"""
    conn = taosws.connect(f"ws://{host}:{port}/rest/sql", user="root", password="taosdata")
    
    conn.execute("CREATE DATABASE IF NOT EXISTS iot_db KEEP 3650")
    conn.execute("USE iot_db")
    
    conn.execute("""
        CREATE STABLE IF NOT EXISTS sensor_data (
            ts TIMESTAMP,
            value FLOAT,
            battery FLOAT,
            quality NCHAR(10)
        ) TAGS (
            device_id NCHAR(20),
            region NCHAR(20),
            sensor_type NCHAR(20),
            factory NCHAR(20)
        )
    """)
    
    for i in range(100):
        device_id = f"sensor-{i:04d}"
        region = ["us-east", "us-west", "eu-central"][i % 3]
        table_name = f"d_sensor_{i:04d}"
        
        sql = f"""
        INSERT INTO {table_name} USING sensor_data 
        TAGS ('{device_id}', '{region}', 'temperature', 'plant-{region.split('-')[0]}')
        VALUES (NOW + {i}a, {25.0 + i * 0.1}, {80.0 + i * 0.2}, 'good')
        """
        conn.execute(sql)
    
    print("Successfully wrote 100 data points to TDengine")
    
    result = conn.execute("SELECT COUNT(*) FROM sensor_data")
    for row in result:
        print(f"Total data count: {row[0]}")
    
    conn.close()


if __name__ == "__main__":
    write_iot_data_tdengine()

Pattern 5: Production-Grade Selection Comparison

# Python: Time series database selection evaluation framework
# Environment: Python 3.12+ / No extra dependencies
from dataclasses import dataclass

@dataclass
class TSDBEvaluation:
    """Time series database selection evaluation model"""
    name: str
    write_throughput_per_sec: int
    query_latency_p99_ms: float
    compression_ratio: float
    sql_compatibility: int
    ecosystem_maturity: int
    cluster_scalability: int
    learning_curve: int
    license_type: str
    best_for: str
    caution: str


def evaluate_tsdb_choices() -> list[TSDBEvaluation]:
    """Generate evaluation results for three major TSDBs"""
    return [
        TSDBEvaluation(
            name="QuestDB",
            write_throughput_per_sec=1_500_000,
            query_latency_p99_ms=15.0,
            compression_ratio=8.5,
            sql_compatibility=9,
            ecosystem_maturity=6,
            cluster_scalability=5,
            learning_curve=9,
            license_type="Apache 2.0",
            best_for="SQL teams, high-frequency writes, financial/IoT real-time analytics",
            caution="Cluster edition requires commercial license, fewer ecosystem plugins",
        ),
        TSDBEvaluation(
            name="InfluxDB",
            write_throughput_per_sec=500_000,
            query_latency_p99_ms=45.0,
            compression_ratio=5.2,
            sql_compatibility=5,
            ecosystem_maturity=9,
            cluster_scalability=7,
            learning_curve=6,
            license_type="MIT / Commercial",
            best_for="DevOps monitoring, Telegraf ecosystem, small-to-medium IoT",
            caution="Steep Flux learning curve, high 3.x migration cost, paid cluster edition",
        ),
        TSDBEvaluation(
            name="TDengine",
            write_throughput_per_sec=2_000_000,
            query_latency_p99_ms=8.0,
            compression_ratio=12.0,
            sql_compatibility=7,
            ecosystem_maturity=7,
            cluster_scalability=8,
            learning_curve=7,
            license_type="AGPL-3.0 / Commercial",
            best_for="Ultra-large-scale IoT, connected vehicles, industrial internet",
            caution="AGPL license restrictions, community edition feature limits, SQL dialect differences",
        ),
    ]


def print_comparison_table(evaluations: list[TSDBEvaluation]):
    """Print selection comparison table"""
    print(f"{'Metric':<30} {'QuestDB':<20} {'InfluxDB':<20} {'TDengine':<20}")
    print("-" * 90)
    
    fields = [
        ("Write throughput (pts/sec)", "write_throughput_per_sec", True),
        ("Query P99 latency (ms)", "query_latency_p99_ms", False),
        ("Compression ratio", "compression_ratio", True),
        ("SQL compatibility (1-10)", "sql_compatibility", True),
        ("Ecosystem maturity (1-10)", "ecosystem_maturity", True),
        ("Cluster scalability (1-10)", "cluster_scalability", True),
        ("Learning curve (1-10)", "learning_curve", True),
        ("License", "license_type", None),
    ]
    
    for label, attr, _ in fields:
        values = [str(getattr(e, attr)) for e in evaluations]
        print(f"{label:<30} {values[0]:<20} {values[1]:<20} {values[2]:<20}")
    
    print("\nBest for:")
    for e in evaluations:
        print(f"  {e.name}: {e.best_for}")
    print("\nCaution:")
    for e in evaluations:
        print(f"  {e.name}: {e.caution}")


if __name__ == "__main__":
    evaluations = evaluate_tsdb_choices()
    print_comparison_table(evaluations)

Pitfall Guide: 5 Common Traps

Trap 1: High-Cardinality Tags Causing OOM

❌ Wrong: Using device_id as an InfluxDB Tag
✅ Right: Put high-cardinality fields in Field, low-cardinality dimensions in Tag

InfluxDB Tags create inverted indexes — 100K device_ids = 100K Series, memory explodes instantly. QuestDB's SYMBOL type and TDengine's super tables solve this natively.

Trap 2: Ignoring Time Zones Causing Aggregation Misalignment

-- ❌ Wrong: No timezone specified, cross-timezone aggregation misaligned
SAMPLE BY 1h

-- ✅ Right: QuestDB timezone alignment
SAMPLE BY 1h ALIGN TO CALENDAR WITH TIME ZONE 'America/New_York'

Trap 3: No Retention Policy Leading to Disk Full

-- InfluxDB: Must set RP
CREATE RETENTION POLICY "7days" ON "sensor-bucket" DURATION 7d REPLICATION 1 DEFAULT

-- TDengine: Specify KEEP when creating database
CREATE DATABASE iot_db KEEP 3650

Trap 4: Improper Batch Write Size

# ❌ Wrong: Writing one by one, massive network overhead
for point in data_points:
    write_api.write(bucket=BUCKET, record=point)

# ✅ Right: Batch write, recommended 5000-10000 points per batch
BATCH_SIZE = 5000
for i in range(0, len(data_points), BATCH_SIZE):
    batch = data_points[i:i + BATCH_SIZE]
    write_api.write(bucket=BUCKET, record=batch)

Trap 5: Confusing TDengine Super Tables with Sub-Tables

-- ❌ Wrong: INSERT directly into super table (no sub-table)
INSERT INTO sensor_data VALUES (NOW, 25.0, 80.0, 'good');

-- ✅ Right: Write through sub-table, auto-inherits super table tags
INSERT INTO d_sensor_0001 USING sensor_data TAGS (...)
VALUES (NOW, 25.0, 80.0, 'good');

Error Troubleshooting Table

Error Message Database Cause Solution
too many series InfluxDB High-cardinality Tag exceeds Series limit Move high-cardinality fields to Field, or upgrade to InfluxDB 3.x
memory limit exceeded QuestDB Query result set too large Add LIMIT, use SAMPLE BY for downsampling
Invalid column type QuestDB SYMBOL column with oversized string Keep SYMBOL length < 64 bytes, or use STRING
table does not exist TDengine Database not selected with USE Execute USE iot_db first
out of memory TDengine Single query memory limit exceeded Adjust queryMemory parameter, narrow time range
connection refused :9009 QuestDB ILP port not enabled Configure tcp.enabled=true
authorization failed InfluxDB Insufficient Token permissions Check Token's read/write permissions
database not found InfluxDB Bucket doesn't exist Create Bucket first or check spelling
invalid timestamp QuestDB Timestamp precision mismatch ILP defaults to nanoseconds, verify timestamp unit
duplicate table name TDengine Sub-table name conflict Ensure sub-table names are globally unique

Advanced Optimization: 5 Production-Grade Tips

Tip 1: Hot/Cold Data Tiered Storage

-- QuestDB: Partition + O3 engine hot/cold separation
ALTER TABLE sensor_data ALTER PARTITION LIST 
    SET ATTRIBUTE 'cold' WHERE timestamp < dateadd('d', -30, now());

-- TDengine: Multi-level storage
ALTER DATABASE iot_db KEEP 30,365,3650;
-- 30 days hot / 365 days warm / 3650 days cold

Tip 2: Pre-Aggregated Materialized Views

-- QuestDB: Materialized view auto-downsampling
CREATE MATERIALIZED VIEW sensor_hourly AS (
    SELECT
        timestamp,
        device_id,
        avg(value) AS avg_value,
        min(value) AS min_value,
        max(value) AS max_value,
        count() AS sample_count
    FROM sensor_data
    SAMPLE BY 1h ALIGN TO CALENDAR
) PARTITION BY DAY;

Tip 3: Write Batching with Backpressure Control

# Python: QuestDB writer with backpressure control
# Environment: Python 3.12+ / pip install questdb
import time
import threading
from queue import Queue, Full
from questdb.ingress import Sender, IngressError

class BatchingWriter:
    """Batch writer with backpressure control"""
    
    def __init__(self, host: str = "localhost", port: int = 9009,
                 batch_size: int = 5000, max_queue: int = 100_000):
        self.batch_size = batch_size
        self.queue = Queue(maxsize=max_queue)
        self.sender = Sender.from_conf(f"http::addr={host}:{port};")
        self._running = True
        self._worker = threading.Thread(target=self._flush_loop, daemon=True)
        self._worker.start()
    
    def write(self, table: str, symbols: dict, columns: dict):
        """Non-blocking write, drops data when queue is full"""
        try:
            self.queue.put_nowait((table, symbols, columns))
        except Full:
            print("Warning: Write queue full, dropping data point")
    
    def _flush_loop(self):
        """Background batch flush"""
        batch = []
        while self._running or not self.queue.empty():
            try:
                item = self.queue.get(timeout=1.0)
                batch.append(item)
                
                if len(batch) >= self.batch_size:
                    self._flush(batch)
                    batch = []
            except Exception:
                if batch:
                    self._flush(batch)
                    batch = []
    
    def _flush(self, batch: list):
        """Execute batch write"""
        try:
            for table, symbols, columns in batch:
                self.sender.row(table, symbols=symbols, columns=columns,
                               at=Sender.current_timestamp_with_nanos())
            self.sender.flush()
            print(f"Flushed {len(batch)} data points")
        except IngressError as e:
            print(f"Write failed: {e}")
    
    def close(self):
        self._running = False
        self._worker.join(timeout=10)
        self.sender.close()

Tip 4: Grafana Visualization Integration

# docker-compose.yml: QuestDB + Grafana integrated deployment
# Environment: Docker Compose v2.30+
version: "3.8"
services:
  questdb:
    image: questdb/questdb:8.2
    ports:
      - "9000:9000"   # Web Console
      - "9009:9009"   # ILP
      - "9003:9003"   # Min health server
    volumes:
      - questdb_data:/root/.questdb
    environment:
      QDB_MALLOC_SIZE: 4G

  grafana:
    image: grafana/grafana:11.4
    ports:
      - "3000:3000"
    environment:
      GF_INSTALL_PLUGINS: grafana-postgresql-datasource
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - questdb

volumes:
  questdb_data:
  grafana_data:

Tip 5: Multi-Active Write and High Availability

# TDengine 3.x cluster deployment
# Environment: TDengine 3.3+ / 3-node cluster
cluster: 1
numOfMnodes: 3
mnodeEqualVnodeNum: 4
statusInterval: 1
maxTablesPerVnode: 1000
minRowsPerBlock: 100
maxRowsPerBlock: 4096

Three TSDBs Comparison Analysis

Dimension QuestDB InfluxDB TDengine
Query Language SQL (with time series extensions) Flux / SQL (3.x) SQL dialect
Write Protocol ILP / REST / PostgreSQL wire ILP / REST REST / WebSocket / JNI
Single Node Write 1.5M pts/sec 500K pts/sec 2M pts/sec
Compression Ratio 8-10x 4-6x 10-15x
Cluster Solution Enterprise edition Open source cluster (3.x) Open source cluster
Grafana Integration ✅ PostgreSQL data source ✅ Native plugin ✅ Native plugin
Kafka Integration ✅ Community ✅ Native ✅ Native
Learning Cost Low (SQL out of the box) Medium-High (unique Flux syntax) Medium (super table concept)
Scale Fit Small-Medium → Large Small-Medium Medium-Large → Ultra-large

Conclusion

There's no silver bullet for IoT time series database selection — it depends on your scenario fit:

  • QuestDB: First choice for SQL teams, high-frequency writes + real-time analytics, lowest learning cost, but cluster edition requires commercial license
  • InfluxDB: Most mature DevOps monitoring ecosystem, Telegraf+Grafana all-in-one, but steep Flux learning curve and high 3.x migration cost
  • TDengine: Ultra-large-scale IoT killer, super tables + compression ratio dominance, but watch out for AGPL license

Selection decision tree: SQL team → QuestDB / DevOps ecosystem → InfluxDB / Ultra-large IoT + localization → TDengine.

Try these browser-local tools — no sign-up required →

#时序数据库#IoT数据#QuestDB#InfluxDB#TDengine#2026#数据库