Deploying QuestDB on Docker: A Guide to High-Velocity Time-Series Data

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Why Standard Databases Break at Scale

Scaling a database for thousands of IoT sensors or high-frequency financial tickers usually follows a predictable, painful pattern. Everything runs smoothly with a few thousand rows. However, once you hit 100 million rows per day, standard relational databases like PostgreSQL or MySQL begin to crawl. Queries that once took 10ms suddenly lag for 15 seconds. Meanwhile, CPU usage spikes to 100% just trying to keep up with the constant stream of incoming writes.

The bottleneck lies in traditional B-Tree indexing and row-oriented storage. While these are perfect for general-purpose CRUD operations, they aren’t designed for append-only, time-ordered data.

Every time you insert a row into a table with multiple indexes, the database must update those structures on disk. This creates massive I/O overhead. Furthermore, if you need to calculate a simple average temperature, a row-oriented database reads the entire row from disk even if you only need one specific column.

QuestDB bypasses these limitations using a columnar storage engine and SIMD (Single Instruction, Multiple Data) instructions to process data in parallel. It treats time as a first-class citizen. On modest 16-core hardware, this architecture allows ingestion rates to exceed 4 million rows per second.

The Landscape: QuestDB vs. The Giants

Choosing a time-series database often comes down to three heavy hitters. Here is how QuestDB compares to the alternatives I see most often in production environments.

QuestDB vs. InfluxDB

InfluxDB is the industry veteran. However, since the 2.x release, it has moved toward Flux, a functional query language. For teams already fluent in SQL, the learning curve for Flux is steep and often unnecessary. QuestDB sticks to standard ANSI SQL with intuitive time-series extensions. In my experience, QuestDB’s storage format is also significantly more efficient, often reducing disk footprints by 30% to 50% compared to InfluxDB for the same dataset.

QuestDB vs. TimescaleDB

TimescaleDB is essentially PostgreSQL with superpowers. It is a reliable alternative if you need full ACID compliance and are already deep in the Postgres ecosystem. But because it relies on the Postgres storage engine, it carries more overhead. QuestDB is a standalone binary built from scratch for speed. If your priority is raw ingestion throughput and sub-millisecond analytical queries, QuestDB usually wins the benchmark.

Pros and Cons

The Advantages

  • SQL Native: Run complex JOINs and aggregations without learning proprietary syntax.
  • Protocol Flexibility: Ingest data via InfluxDB Line Protocol (ILP), PostgreSQL Wire, or REST.
  • Zero-Copy Transfers: It uses memory-mapped files to provide lightning-fast data access.
  • Instant Visualization: The built-in web console lets you go from zero to data visualization in seconds.

The Drawbacks

  • Library Size: The ecosystem is growing, but it lacks the massive third-party plugin library found in PostgreSQL.
  • Schema Rigidity: Unlike NoSQL, you must define a “designated timestamp” column early to get the best performance.
  • RAM Hungry: Because it relies on memory mapping, performance degrades quickly if your server doesn’t have enough RAM to map active data parts.

The Setup: Docker and Docker Compose

Docker is the most efficient way to deploy QuestDB for development or mid-sized production workloads. It isolates the environment and makes versioning painless. Always use a dedicated volume for persistence; otherwise, a container restart will wipe your entire database.

When working with raw data exports, I often need to convert messy CSV files into clean JSON for API testing. I use toolcraft.app/en/tools/data/csv-to-json for this. It runs entirely in the browser, ensuring that sensitive financial or sensor data never leaves your local machine.

Implementation Guide

1. Launching QuestDB

Create a project directory and a docker-compose.yml file. This configuration ensures your data persists in a local folder called questdb_data.

mkdir questdb-project && cd questdb-project
touch docker-compose.yml

Add this configuration to your file:

services:
  questdb:
    image: questdb/questdb:latest
    container_name: questdb
    restart: always
    ports:
      - "9000:9000"   # Web Console
      - "9009:9009"   # ILP (TCP)
      - "8812:8812"   # Postgres Wire
      - "9003:9003"   # Metrics
    volumes:
      - ./questdb_data:/var/lib/questdb
    environment:
      - QDB_TELEMETRY_ENABLED=false

Fire up the container:

docker-compose up -d

Access the UI immediately at http://localhost:9000.

2. Designing the Table

The “Designated Timestamp” is the secret sauce. It tells QuestDB how to partition data on the disk. For IoT data, use the following schema:

CREATE TABLE sensors (
    device_id SYMBOL,
    temperature DOUBLE,
    humidity DOUBLE,
    timestamp TIMESTAMP
) TIMESTAMP(timestamp) PARTITION BY DAY WAL;

Key details:

  • SYMBOL: This converts strings like “sensor_01” into internal integers. It saves massive amounts of disk space and accelerates filtering.
  • PARTITION BY DAY: This organizes data into daily files. It makes dropping old data as simple as deleting a file.
  • WAL: Write-Ahead Logging ensures data consistency even during unexpected power failures.

3. High-Speed Ingestion with Python

While SQL INSERT works, the InfluxDB Line Protocol (ILP) is built for speed. Install the client library first:

pip install questdb

Use this script to stream data:

from questdb.ingress import Sender, TimestampNanos

def stream_data():
    try:
        with Sender('localhost', 9009) as sender:
            for i in range(100):
                sender.row(
                    'sensors',
                    symbols={'device_id': f'sensor_{i}'},
                    columns={'temperature': 20.0 + (i * 0.1), 'humidity': 45.0},
                    at=TimestampNanos.now()
                )
            sender.flush()
            print("Batch ingested.")
    except Exception as e:
        print(f"Ingestion failed: {e}")

if __name__ == "__main__":
    stream_data()

4. Querying with SAMPLE BY

QuestDB shines when aggregating time-series data. To find the average temperature per hour over the last 24 hours, use the SAMPLE BY keyword:

SELECT timestamp, avg(temperature) 
FROM sensors 
WHERE timestamp > dateadd('d', -1, now()) 
SAMPLE BY 1h;

This is significantly faster than a standard GROUP BY. Because the data is already partitioned by time, QuestDB only scans the exact blocks of data required for that specific range.

Final Thoughts

Deploying QuestDB via Docker provides an immediate performance boost for any project handling time-sensitive data. Whether you are monitoring a fleet of 50,000 devices or building a real-time crypto dashboard, the combination of SQL ease-of-use and high-speed ingestion is hard to beat. Just remember to pick your PARTITION BY strategy (Day, Month, or Year) based on your expected data volume to keep queries snappy as your dataset grows.

Share: