DEV Community

Engr.Hamza
Engr.Hamza

Posted on

How to Validate Event Data in ClickHouse and Superset Without Losing Your Mind

Cover Image

How to Validate Event Data in ClickHouse and Superset Without Losing Your Mind

Every data engineer eventually hits the wall where analytics dashboards start showing negative user counts, revenue metrics drop to zero because of a rogue string, and stakeholders are breathing down your neck. When you are streaming millions of telemetry events into ClickHouse and visualizing them through Apache Superset, garbage in does not just mean garbage out—it means complete dashboard paralysis. If you have ever had to explain to your CEO why active users dropped by 80% overnight only to find out a frontend developer changed a property type from an integer to a null string, you know the exact kind of dread I am talking about.


The Problem Everyone Ignores

Most teams treat data pipelines like a wild west, assuming that ingestion engines like ClickHouse will just handle whatever unstructured soup you throw at them. Because ClickHouse is lightning-fast and schema-flexible, developers often adopt a "write now, fix later" mentality, pushing raw JSON strings into String columns or relying on dynamic types.

Architecture Overview

Above: High-level architecture overview of the topic covered in this article.

The real pain starts downstream when you connect Apache Superset to build executive dashboards. Superset expects clean, predictable data types to render charts, calculate aggregations, and apply filters correctly. When a single schema drift occurs—like a timestamp format changing from epoch milliseconds to an ISO-8601 string—Superset queries instantly throw cryptic exceptions, or worse, silently drop rows containing malformed data.

You end up wasting hours writing messy, performance-killing SQL CASE statements and CAST transformations inside your Superset virtual datasets just to sanitize fields that should have never entered the database in that state. By the time your pipeline grows to handle billions of events daily, cleaning data downstream becomes an impossible bottleneck. The storage engine might handle the write throughput effortlessly, but your analytics layer grinds to a complete halt, breaking trust across the entire organization.


What Actually Works

To solve this permanently, we need to shift our validation left and enforce strict structural contracts right at the ingestion boundary before data ever hits our persistent ClickHouse tables. Instead of trusting upstream applications to send pristine payloads, we can leverage ClickHouse's powerful table engines, specifically the Distributed and Materialized View pattern combined with JSON functions and conditional parsing, to filter, sanitize, and reject bad events at the gate.

The secret to a bulletproof pipeline is separating your raw ingestion landing zone from your production analytics tables. By capturing incoming streams into an unparsed landing table, we can use a Materialized View as an automated validation pipeline that parses, type-checks, and discards malformed payloads before they pollute our clean datasets. This ensures that Superset always queries a predictable, strictly typed schema where aggregates never fail and charts load instantly.

Here is how you can set up a resilient validation layer directly inside ClickHouse using table engines and conditional parsing functions:

CREATE DATABASE IF NOT EXISTS telemetry;

-- Raw landing table for incoming unstructured JSON strings
CREATE TABLE telemetry.events_raw (
    raw_payload String,
    ingested_at DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY ingested_at;

-- Clean, strictly typed production table
CREATE TABLE telemetry.events_clean (
    event_id UUID,
    user_id UInt64,
    event_name LowCardinality(String),
    session_duration Float32,
    event_date Date DEFAULT toDate(event_timestamp),
    event_timestamp DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_name, user_id);
Enter fullscreen mode Exit fullscreen mode

This code sets up a two-tier architecture where raw ingestion is decoupled from analytical consumption, giving us a safe sandbox to intercept and validate data.


Step-by-Step: Let's Build It Together

Now that we have our tables ready, let us walk through the implementation of the automated validation pipeline. We need a mechanism that intercepts rows from events_raw, extracts the fields safely, drops invalid records, and inserts clean rows into events_clean.

First, we create a Materialized View that utilizes ClickHouse's JSON extraction functions along with strict type casting guards. If a field fails type coercion—for instance, if user_id contains letters instead of numbers—ClickHouse handles it gracefully or drops the row based on our validation logic.

Here is the exact code for the validation view:

CREATE MATERIALIZED VIEW telemetry.events_validator_mv
TO telemetry.events_clean AS
SELECT
    -- Safely parse UUID or generate a fallback/skip
    toUUIDOrNull(JSONExtractString(raw_payload, 'event_id')) AS event_id,

    -- Ensure user_id is strictly numerical, filter out text
    toInt64OrZero(JSONExtractString(raw_payload, 'user_id')) AS user_id,

    -- Extract event name with a default fallback
    coalesce(JSONExtractString(raw_payload, 'event_name'), 'unknown_event') AS event_name,

    -- Parse session duration safely as float
    toFloat32OrZero(JSONExtractString(raw_payload, 'session_duration')) AS session_duration,

    -- Parse timestamp, defaulting to current time if parsing fails
    coalesce(toDateTimeOrNull(JSONExtractString(raw_payload, 'timestamp')), now()) AS event_timestamp
FROM telemetry.events_raw
WHERE event_id IS NOT NULL 
  AND user_id > 0;
Enter fullscreen mode Exit fullscreen mode

What just happened here is that every single insert into events_raw automatically triggers this query, transforming unstructured JSON into strictly typed columns while filtering out corrupted records at the database level.

Next, we need a way to track what is failing so our engineering team can fix upstream producers. We will create a dead-letter table to catch rejected payloads for auditing and debugging.

Here is the dead-letter queue setup:

CREATE TABLE telemetry.events_dlq (
    raw_payload String,
    rejection_reason String,
    rejected_at DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY rejected_at;

CREATE MATERIALIZED VIEW telemetry.events_dlq_mv
TO telemetry.events_dlq AS
SELECT
    raw_payload,
    CASE
        resting
        WHEN toUUIDOrNull(JSONExtractString(raw_payload, 'event_id')) IS NULL THEN 'Invalid UUID'
        WHEN toInt64OrZero(JSONExtractString(raw_payload, 'user_id')) <= 0 THEN 'Invalid User ID'
        ELSE 'Unknown Parsing Error'
    END AS rejection_reason
FROM telemetry.events_raw
WHERE toUUIDOrNull(JSONExtractString(raw_payload, 'event_id')) IS NULL
   OR toInt64OrZero(JSONExtractString(raw_payload, 'user_id')) <= 0;
Enter fullscreen mode Exit fullscreen mode

This second materialized view captures every bad event, tags it with a clear failure reason, and stores it in our dead-letter queue for quick root-cause analysis.


The Mistakes That Will Burn You

When implementing data validation in ClickHouse and Superset, teams often fall into traps that compromise performance or data integrity. Here are the most common pitfalls you need to avoid:

  • Mistake 1: Relying solely on Superset dataset expressions to fix bad types. Doing this pushes heavy computational overhead to every single dashboard query, slowing down Superset renders and frustrating your users.
  • Mistake 2: Using strict JSONExtract without safe fallback functions like toInt64OrNull or toUUIDOrNull. If a malformed payload hits a strict extraction function, the entire batch insert fails, causing massive pipeline backpressure and data loss.
  • Mistake 3: Ignoring the dead-letter queue monitoring. Simply dropping invalid events into a table without setting up alerts means you will never know when an upstream client breaks their telemetry schema.

Production Checklist

Before you push this architecture to your production cluster and connect your executive Superset dashboards, verify these core items:

  • Verify data types: Ensure every column in your clean table matches the expected Superset dataset definitions to prevent visualization errors.
  • Monitor ingestion lag: Check your ClickHouse system tables to ensure materialized views are keeping up with raw stream ingestion rates.
  • Test failure modes: Send deliberately malformed JSON payloads to your landing table to confirm they are successfully routed to the dead-letter queue.
  • Never do this: Never expose raw unvalidated JSON string columns directly to end-user Superset datasets for aggregation.

Key Takeaways

  • Decouple your ingestion landing zone from your analytics layer by using raw string landing tables.
  • Leverage ClickHouse safe parsing functions (toUUIDOrNull, toInt64OrZero) inside Materialized Views to filter out bad data automatically.
  • Maintain a dedicated dead-letter queue table to audit rejected events and catch upstream schema breaking changes early.
  • Keep Superset performant by feeding it strictly typed, pre-validated tables rather than forcing it to clean data on the fly.

Engr. Hamza | AI & MLOps Engineer | Building autonomous systems at the edge of possibility

Top comments (0)