How to Design Modern OLAP Architectures with ClickHouse and Apache Druid: A Complete Engineering Guide

Modern OLAP architectures combine ClickHouse for high-throughput batch analytics with Apache Druid for sub-second latency on streaming data, using shared Kafka ingestion pipelines and unified schema registries to handle petabyte-scale workloads.

Designing modern OLAP architectures with ClickHouse and Apache Druid requires understanding their complementary strengths within the DataExpert-io/data-engineer-handbook ecosystem. While both are columnar stores optimized for analytical workloads, ClickHouse excels at complex SQL aggregations across massive datasets, whereas Druid specializes in real-time ingestion with time-series pruning and low-latency filtering. This guide examines their architectural differences, provides a hybrid reference implementation, and includes production-ready Python code samples from the handbook's official repository.

Core Architectural Differences

Understanding the fundamental design philosophies of each engine is critical when designing modern OLAP architectures with ClickHouse and Apache Druid.

ClickHouse Design Philosophy

ClickHouse is a distributed columnar DBMS built on a vectorized execution engine and MergeTree storage architecture. Queries execute in parallel across shards, leveraging SIMD instructions for fast aggregations. Unlike traditional databases, ClickHouse is primary key-less by default, instead using data skipping indexes (min/max, bloom filters) and sorting keys for fast range scans. According to the README.md in the DataExpert-io/data-engineer-handbook repository, ClickHouse serves as the preferred engine for high-performance dashboards and ad-tech analytics.

Apache Druid Design Philosophy

Apache Druid operates as a real-time columnar store that combines a streaming ingestion pipeline with immutable segment files based on Apache Parquet/ORC formats. Druid employs a pluggable query engine supporting both SQL (via Apache Calcite) and native Druid JSON queries. Its architecture relies on multi-dimensional bitmap and inverted indexes, with segment granularity (hour/day) enabling aggressive time-based pruning. The projects.md file demonstrates Druid's practical application for real-time visualization pipelines using Apache Superset.

Ingestion Patterns and Indexing Strategies

Data Ingestion Architectures

Both engines support Kafka as a primary ingestion source, but their approaches differ significantly:

  • ClickHouse: Optimized for bulk batch loads (INSERT, CSV, Parquet) with secondary support for Kafka streams via table engines. Best suited for scenarios where data lands in S3/HDFS before analytical processing.
  • Apache Druid: Native Kafka and Kinesis real-time ingestion with exactly-once semantics. Batch imports occur via dedicated batch indexing jobs, making Druid ideal for event-driven architectures requiring immediate query availability.

Indexing Mechanisms

The indexing strategies define query performance characteristics:

  • ClickHouse: Uses sparse primary indexes and secondary data skipping indexes. Data is physically sorted by the ORDER BY clause defined in the MergeTree engine configuration, enabling fast range scans without heavy memory overhead.
  • Druid: Automatically builds multi-dimensional bitmap indexes at ingestion time. The segment-based architecture creates inverted indexes for all dimensions, enabling microsecond-level filtering on high-cardinality columns.

Reference Architecture for Hybrid OLAP

The DataExpert-io/data-engineer-handbook recommends a dual-engine architecture that couples both stores in a single analytics platform, allowing teams to select the optimal engine per query pattern.


          +-------------------+      +-------------------+
          |   Streaming Data  |      |   Batch Data Lake |
          | (Kafka / Kinesis) |      |  (S3 / HDFS)      |
          +--------+----------+      +--------+----------+
                   |                         |
          +--------v----------+    +---------v-----------+
          |  Ingestion Layer   |    |   Ingestion Layer   |
          | (Flink / Spark)    |    | (Spark / DBT)       |
          +--------+----------+    +----------+----------+
                   |                         |
          +--------v----------+    +----------v----------+
          |   Real‑time store |    |   Columnar store   |
          |  (Apache Druid)   |    | (ClickHouse)       |
          +--------+----------+    +----------+----------+
                   |                         |
          +--------v----------+    +----------v----------+
          |  Query Engine      |    |  Query Engine       |
          | (SQL / Druid JSON) |    | (SQL)               |
          +--------+----------+    +----------+----------+
                   |                         |
          +--------v----------+    +----------v----------+
          |   BI / Dashboard   |    |   BI / Dashboard    |
          | (Superset, Tableau)|    | (Metabase, PowerBI)|
          +-------------------+    +--------------------+

Key Design Decisions

When implementing this architecture from the intermediate-bootcamp/materials/6-data-impact-training/README.md patterns, adhere to these principles:

  1. Separate workloads – Use Druid for low-latency, high-cardinality, time-series queries (e.g., "active users per minute") and ClickHouse for heavy-aggregation, ad-hoc analytical queries (e.g., "monthly churn by segment").

  2. Unified data model – Store raw events in an immutable lake (S3/HDFS). Both engines ingest from the same source, preserving a single source of truth as outlined in the handbook's pipeline guidelines.

  3. Stream-first ingestion – Deploy a Flink job that fans out each event to both Kafka topics: one consumed by Druid's real-time indexing service, the other by a batch pipeline that writes to ClickHouse. The intermediate-bootcamp/materials/3-spark-fundamentals/docker-compose.yaml provides boilerplate infrastructure for testing this dual-stream pattern.

  4. Metadata governance – Leverage a schema registry (e.g., Confluent Schema Registry) to keep column definitions synchronized across both stores, preventing schema drift between the real-time and batch paths.

  5. Caching & materialized views – Use ClickHouse materialized views for pre-aggregated tables; Druid automatically materializes segment-level indexes during the ingestion phase.

Implementation Guide

The following snippets illustrate a minimal Python-based pipeline that writes the same JSON event to both stores, adapted from the patterns in intermediate-bootcamp/materials/5-kpis-and-experimentation/README.md.

Setting Up Client Libraries

pip install clickhouse-driver pydruid

Writing to ClickHouse

from clickhouse_driver import Client

# Connect to ClickHouse

ch = Client(host='clickhouse.mycompany.com', port=9000,
            user='default', password='')

# Create a simple table (run once)

ch.execute('''
CREATE TABLE IF NOT EXISTS analytics.events (
    event_time DateTime,
    user_id    UInt64,
    event_type String,
    value      Float64
) ENGINE = MergeTree()
ORDER BY (event_time, user_id);
''')

def write_clickhouse(event):
    ch.execute(
        'INSERT INTO analytics.events (event_time, user_id, event_type, value) VALUES',
        [(event['timestamp'], event['user_id'],
          event['type'], event['value'])]
    )

Indexing into Apache Druid

from pydruid.client import PyDruid
from pydruid.ingestion import IndexSpec

# Druid endpoint

druid = PyDruid('http://druid.mycompany.com:8082', 'druid/v2')

def write_druid(event):
    # Build a simple ingestion spec (batch example)

    spec = IndexSpec(
        dataSource='events',
        timestampSpec={'column': 'timestamp', 'format': 'iso'},
        dimensionsSpec={'dimensions': ['user_id', 'event_type']},
        metricsSpec=[{'type': 'doubleSum', 'name': 'value', 'fieldName': 'value'}],
        granularitySpec={'type': 'uniform', 'segmentGranularity': 'hour'}
    )
    # Post the spec – Druid will ingest the data (normally via a Kafka task)

    druid.post_index(spec.to_dict())

Unified Producer Pattern

import json
import time

def produce(event):
    # Write to both stores

    write_clickhouse(event)
    write_druid(event)

# Example event

sample = {
    "timestamp": "2024-08-12T15:30:00Z",
    "user_id": 12345,
    "type": "click",
    "value": 1.0
}

produce(sample)

Note: In production, replace the direct Druid post_index call with a Kafka ingestion task that continuously streams events, as recommended in the projects.md implementation examples.

Repository Structure and Resources

The DataExpert-io/data-engineer-handbook repository contains specific resources for implementing this architecture:

Summary

  • ClickHouse provides superior throughput for complex SQL aggregations and ad-hoc analytics using vectorized execution and MergeTree storage engines.
  • Apache Druid delivers sub-second query latency for streaming data through bitmap indexing and time-based segment pruning.
  • Hybrid architectures leverage both engines simultaneously, routing real-time dashboards to Druid and heavy analytical workloads to ClickHouse.
  • Unified ingestion via Kafka and Flink ensures data consistency across both platforms while maintaining a single source of truth in your data lake.
  • Schema registries and materialized views are essential for maintaining performance and consistency in dual-engine OLAP systems.

Frequently Asked Questions

When should I choose ClickHouse over Druid for my OLAP workload?

Choose ClickHouse when your workload involves complex SQL queries with heavy aggregations, joins, and sub-queries across large historical datasets. ClickHouse excels at batch analytics where query flexibility and ANSI-SQL compatibility are priorities. Choose Druid when you need sub-second latency on streaming data with high-cardinality filtering and time-series analysis, particularly for real-time monitoring dashboards and anomaly detection systems.

How do I handle schema evolution in a dual-engine OLAP architecture?

Implement a Confluent Schema Registry or similar metadata governance layer that both engines reference during ingestion. Store raw events in an immutable format (Parquet/ORC) in S3 or HDFS as your single source of truth. When schema changes occur, update the registry first, then modify ingestion specifications in both ClickHouse and Druid to accommodate new fields while maintaining backward compatibility for existing queries.

What hardware considerations apply when running both ClickHouse and Druid together?

Druid is memory-heavy due to its reliance on bitmap indexes and in-memory caching for real-time segments; allocate nodes with high RAM (256GB+) for historical and real-time services. ClickHouse is disk-optimized and benefits from fast SSD storage for MergeTree data parts, though it can run efficiently on commodity hardware with less memory. For cost efficiency, store hot recent data in Druid (high-memory instances) and archive older data to ClickHouse (disk-optimized instances).

Can I use ClickHouse alone instead of Druid for real-time streaming analytics?

While ClickHouse supports Kafka ingestion via the Kafka table engine and materialized views, it lacks Druid's native real-time indexing service and automatic time-based segment pruning. For true real-time OLAP requiring sub-second latency on high-cardinality dimensions, Druid's architecture is optimized specifically for this use case. However, for near-real-time analytics (seconds to minutes delay) with simpler filtering requirements, ClickHouse alone may suffice and reduce operational complexity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →