PostHog Handbook Library / Engineering

582 words. Estimated reading time: 3 min.

Data ingestion

This document covers:

Using INSERTs for ingestion

As any database system, ClickHouse allows using INSERTs to load data.

Each INSERT creates a new part in ClickHouse, which comes with a lot of overhead and, in a busy system, will lead to errors due to exceeding parts_to_throw MergeTree table setting (default 300).

ClickHouse provides a bunch of options to make INSERTs still work. For example:

These come with their own trade-offs, consistency problems, and require the ClickHouse cluster to always be accessible.

Why we ingest via Kafka tables

We instead rely on the Kafka table engine to handle ingestion into ClickHouse.

The benefits are:

It also has minimal overhead in terms of memory used and allows us to always temporarily stop ingestion by removing the tables in question.

How Kafka tables work

Kafka engine tables act as Kafka consumers in a given consumer group. Selecting from that table advances the consumer offsets.

A Kafka table on its own does nothing beyond allowing querying data from Kafka – it needs to be paired with other tables for ingestion to work.

Important note: Given Kafka engine tables operate like consumers, querying data from them moves the offsets for the consumer group forward. Doing this while ingesting data may cause data loss, and has been disallowed by default on the latest ClickHouse versions.

Example Kafka engine table:

CREATE TABLE kafka_ingestion_warnings
(
    team_id Int64,
    source LowCardinality(VARCHAR),
    type VARCHAR,
    details VARCHAR CODEC(ZSTD(3)),
    timestamp DateTime64(6, 'UTC')
)
ENGINE = Kafka('kafka:9092', 'clickhouse_ingestion_warnings_test', 'group1', 'JSONEachRow')

It is important to send correctly formatted messages to the topic you're selecting from. When selecting from a Kafka table, ClickHouse assumes messages in the topic are formatted correctly. If not, this may stall the consumer depending on the value of kafka_skip_broken_messages, breaking ingestion.

Beyond just skipping broken messages, it's also possible to set up a dead letter queue system for these in ClickHouse. You can read more about doing so in this Altinity blog post.

Materialized views

Materialized views in ClickHouse can be thought of as triggers – they react to new blocks being INSERTed into source tables and allow transforming and piping that data to other tables.

Materialized views come with a lot of gotchas. A great resource for learning more about them is this presentation.

Example schema – reading and writing ingestion events

Consider the following sharded table schema together with kafka_ingestion_warnings:

CREATE TABLE sharded_ingestion_warnings
(
    team_id Int64,
    source LowCardinality(VARCHAR),
    type VARCHAR,
    details VARCHAR CODEC(ZSTD(3)),
    timestamp DateTime64(6, 'UTC'),
    _timestamp DateTime,
    _offset UInt64,
    _partition UInt64
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/posthog.sharded_ingestion_warnings', '{replica}')
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (team_id, toHour(timestamp), type, source, timestamp)

CREATE TABLE ingestion_warnings ON CLUSTER 'posthog'
(
    team_id Int64,
    source LowCardinality(VARCHAR),
    type VARCHAR,
    details VARCHAR CODEC(ZSTD(3)),
    timestamp DateTime64(6, 'UTC'),
    _timestamp DateTime,
    _offset UInt64,
    _partition UInt64
)
ENGINE = Distributed('posthog', 'posthog', 'sharded_ingestion_warnings', rand())

CREATE MATERIALIZED VIEW ingestion_warnings_mv
TO posthog.ingestion_warnings
AS SELECT
    team_id,
    source,
    type,
    details,
    timestamp,
    _timestamp,
    _offset,
    _partition
FROM posthog.kafka_ingestion_warnings

In this schema:

Example schema visualized

This is the same schema visualized in a ClickHouse cluster with 2 shards and 1 replica each:

flowchart TB
    classDef table fill:#FAEEDA,stroke:#854F0B,color:#412402;
    classDef kafka fill:#EEEDFE,stroke:#534AB7,color:#26215C;

    Kafka["<b>Kafka</b><br/>clickhouse_events_proto topic"]:::kafka

    subgraph CH ["ClickHouse cluster"]
        subgraph CH1 ["Shard 1, replica 1"]
            kafka_ingestion_warnings1["<b>kafka_ingestion_warnings</b><br/>Kafka table engine"]:::table
            ingestion_warnings_mv1["<b>ingestion_warnings_mv</b><br/>Materialized view"]:::table
            ingestion_warnings1["<b>ingestion_warnings</b><br/>Distributed table engine"]:::table
            sharded_ingestion_warnings1["<b>sharded_ingestion_warnings</b><br/>ReplicatedMergeTree engine"]:::table
        end
        subgraph CH2 ["Shard 2, replica 1"]
            kafka_ingestion_warnings2["<b>kafka_ingestion_warnings</b><br/>Kafka table engine"]:::table
            ingestion_warnings_mv2["<b>ingestion_warnings_mv</b><br/>Materialized view"]:::table
            ingestion_warnings2["<b>ingestion_warnings</b><br/>Distributed table engine"]:::table
            sharded_ingestion_warnings2["<b>sharded_ingestion_warnings</b><br/>ReplicatedMergeTree engine"]:::table
        end
    end

    style CH fill:#F1EFE8,stroke:#B4B2A9,color:#2C2C2A
    style CH1 fill:#FCFBF8,stroke:#D3D1C7,color:#5F5E5A
    style CH2 fill:#FCFBF8,stroke:#D3D1C7,color:#5F5E5A

    Kafka -..- kafka_ingestion_warnings1
    Kafka -..- kafka_ingestion_warnings2

    kafka_ingestion_warnings1 --> ingestion_warnings_mv1
    kafka_ingestion_warnings2 --> ingestion_warnings_mv2
    ingestion_warnings_mv1 --> ingestion_warnings1
    ingestion_warnings_mv2 --> ingestion_warnings2

    ingestion_warnings1 -.-> sharded_ingestion_warnings1
    ingestion_warnings1 -.-> sharded_ingestion_warnings2
    ingestion_warnings2 -.-> sharded_ingestion_warnings1
    ingestion_warnings2 -.-> sharded_ingestion_warnings2

    linkStyle 6,7,8,9 stroke:#BA7517

Further reading

about materialized views.](https://den-crane.github.io/Everything_you_should_know_about_materialized_views_commented.pdf)

Next in the ClickHouse manual: Working with JSON

Canonical URL: https://posthog.com/handbook/engineering/clickhouse/data-ingestion

GitHub source: contents/handbook/engineering/clickhouse/data-ingestion.mdx

Content hash: e026d2e9451e0f2b