<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nishaant dixit</title>
    <description>The latest articles on DEV Community by nishaant dixit (@heleo).</description>
    <link>https://dev.to/heleo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901087%2Ffa11c8f5-7c2c-43d5-8726-4cc8f7ff6bcd.png</url>
      <title>DEV Community: nishaant dixit</title>
      <link>https://dev.to/heleo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heleo"/>
    <language>en</language>
    <item>
      <title>ClickHouse Consulting for Startups: What Nobody Tells You About Scaling Analytics</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Fri, 08 May 2026 08:33:21 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-consulting-for-startups-what-nobody-tells-you-about-scaling-analytics-2412</link>
      <guid>https://dev.to/heleo/clickhouse-consulting-for-startups-what-nobody-tells-you-about-scaling-analytics-2412</guid>
      <description>&lt;p&gt;Two years ago, a Series A startup came to me with a problem. Their PostgreSQL database was buckling under 50GB of event data. Queries took minutes. Their CEO was screaming for real-time dashboards.&lt;/p&gt;

&lt;p&gt;They hired a consulting firm that proposed a Kafka-to-ClickHouse pipeline. Cost: $80K. Timeline: four months.&lt;/p&gt;

&lt;p&gt;I told them they could do it themselves in two weeks with the right guidance.&lt;/p&gt;

&lt;p&gt;They didn't believe me. Until they tried it.&lt;/p&gt;

&lt;p&gt;Here's what I've learned about ClickHouse consulting for startups: most advice you'll find online is written for enterprises with infinite resources. Startups need something different. This guide covers what actually works when you're moving fast and burning cash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse consulting?&lt;/strong&gt; It's specialized guidance for designing, deploying, and optimizing ClickHouse – the open-source columnar database built for real-time analytics on massive datasets. For startups, it means skipping the boilerplate and getting to production without the enterprise overhead.&lt;/p&gt;




&lt;p&gt;ClickHouse isn't another SQL database. It's a columnar OLAP engine designed for analytical workloads. Think aggregations, time-series data, and log analytics – not transactional processing.&lt;/p&gt;

&lt;p&gt;The core architecture breaks down like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Columnar storage&lt;/strong&gt; – Data is stored by column, not row. This means queries that touch a few columns read far less data from disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vectorized execution&lt;/strong&gt; – CPU caches are optimized by processing data in batches (vectors) rather than row-by-row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared-nothing architecture&lt;/strong&gt; – Each node manages its own data. Scaling is horizontal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most startups miss the critical distinction: ClickHouse is not PostgreSQL. You cannot treat it like one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard truth:&lt;/strong&gt; I've seen teams dump JSON blobs into ClickHouse and expect sub-second queries. It doesn't work that way. ClickHouse demands schema design upfront.&lt;/p&gt;

&lt;p&gt;Here's a real schema from a startup I helped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- JSON blob, bad idea&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my experience, the &lt;code&gt;properties&lt;/code&gt; column as a string is the number one mistake. Parse JSON into native columns during ingestion. ClickHouse's &lt;code&gt;JSONExtract&lt;/code&gt; functions work, but they kill performance on large scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;LowCardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_duration&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LowCardinality&lt;/code&gt; type is a startup's best friend. It compresses strings representing limited distinct values (like event types) into dictionary-encoded integers. This cuts storage by 80% and speeds up scans.&lt;/p&gt;




&lt;p&gt;Startups need three things from their analytics stack: speed, cost-efficiency, and simplicity. ClickHouse delivers on all three, but only when configured correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed&lt;/strong&gt; – ClickHouse can scan billions of rows in sub-seconds. According to the &lt;a href="https://clickhouse.com/benchmark/dbms" rel="noopener noreferrer"&gt;Clickhouse official benchmarks&lt;/a&gt;, it outperforms PostgreSQL by 100-200x on typical analytical queries. A startup processing 10M events daily can run complex aggregations in real-time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; – Columnar compression is aggressive. I've seen startups reduce storage costs by 10x compared to PostgreSQL. A 100GB PostgreSQL table might compress to 8GB in ClickHouse. At $0.10/GB/month cloud storage, that's real money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplicity&lt;/strong&gt; – One binary, no dependencies. ClickHouse runs on a single server. For early-stage startups, this means no need for complex cluster management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real use case:&lt;/strong&gt; A fintech startup I consulted needed to surface fraud patterns across 5M transactions daily. Their Django app used PostgreSQL. Fraud queries took 45 seconds. We stood up a single ClickHouse node, routed transaction data via Kafka, and queries dropped to 200ms. The entire migration took three days.&lt;/p&gt;

&lt;p&gt;The trade-off? ClickHouse excels at bulk inserts. Single-row inserts are slow. Batch inserts of 100K rows are fast. This pattern requires rethinking how your application writes data.&lt;/p&gt;




&lt;p&gt;Let's get concrete. Here's how you actually deploy ClickHouse for startup workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Single-node with replication to object storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start with one production node. Configure backups to S3 or GCS using ClickHouse's built-in &lt;code&gt;BACKUP&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;BACKUP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'/backups/events/'&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; 
    &lt;span class="n"&gt;compression_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'lz4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compression_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pattern 2: Kafka ingestion pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Event data streams naturally into ClickHouse via Kafka. The &lt;code&gt;Kafka&lt;/code&gt; engine table acts as a bridge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_kafka&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Kafka&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt;
    &lt;span class="n"&gt;kafka_broker_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'localhost:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_topic_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clickhouse'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'JSONEachRow'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Materialized view writes to target table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;events_mv&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_kafka&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Kafka consumers in ClickHouse run in-process. If the node crashes, offsets reset. Add &lt;code&gt;kafka_auto_offset_reset = 'earliest'&lt;/code&gt; as a safety net.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: Optimizing for time-series data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Startups with IoT or logging workloads should leverage ClickHouse's time-series optimizations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cpu_usage&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_usage&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;disk_io&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Use AggregatingMergeTree for pre-aggregated data&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics_hourly&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;toStartOfHour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avg_cpu&lt;/span&gt; &lt;span class="n"&gt;SimpleAggregateFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;max_mem&lt;/span&gt; &lt;span class="n"&gt;SimpleAggregateFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AggregatingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;TTL&lt;/code&gt; clause auto-deletes data older than 90 days. The &lt;code&gt;AggregatingMergeTree&lt;/code&gt; stores pre-computed hourly stats. Queries against the aggregated table run 50x faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt; Using &lt;code&gt;ORDER BY&lt;/code&gt; on high-cardinality columns like &lt;code&gt;user_id&lt;/code&gt; alone. In my experience, always prefix the sort key with a low-cardinality column. &lt;code&gt;ORDER BY (event_type, user_id)&lt;/code&gt; beats &lt;code&gt;ORDER BY (user_id)&lt;/code&gt; by 4x on range scans.&lt;/p&gt;




&lt;p&gt;After working with 15+ startups on ClickHouse implementations, here are the patterns that separate success from failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Schema design is non-negotiable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Research from &lt;a href="https://altinity.com/blog/migrating-from-redshift-to-clickhouse" rel="noopener noreferrer"&gt;Altinity's migration guide&lt;/a&gt; shows that schema redesign accounts for 60% of migration complexity. Don't skip this step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;LowCardinality&lt;/code&gt; for strings with fewer than 10K distinct values&lt;/li&gt;
&lt;li&gt;Prefer integers over strings for IDs&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;Nullable&lt;/code&gt; columns – they prevent certain optimizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Monitor query performance religiously&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse exposes system tables for everything. I set up alerts on &lt;code&gt;system.query_log&lt;/code&gt; for queries taking longer than 1 second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Batch your inserts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 2025 benchmark from &lt;a href="https://double.cloud/blog/posts/2025/01/how-to-migrate-from-postgresql-to-clickhouse/" rel="noopener noreferrer"&gt;DoubleCloud's migration guide&lt;/a&gt; demonstrated that inserting 100K rows in one batch is 100x faster than 100K individual inserts. Use a buffer like &lt;code&gt;Buffer&lt;/code&gt; engine for high-frequency writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Understand when NOT to use ClickHouse&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse fails at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time point lookups (use Redis)&lt;/li&gt;
&lt;li&gt;Row-level updates and deletes (use PostgreSQL)&lt;/li&gt;
&lt;li&gt;Complex joins on non-distributed tables (keep tables denormalized)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Should you hire a ClickHouse consultant or figure it out yourself?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build in-house:&lt;/strong&gt; Doable if you have one engineer with 2+ years of database experience. Expect 3-4 weeks to production. Budget: 2-4 weeks of engineering time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hire a consultant:&lt;/strong&gt; Necessary if your data volume exceeds 100M rows daily or you need HA. Expect 1-2 weeks engagement. Budget: $10K-$30K.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed services:&lt;/strong&gt; Options like ClickHouse Cloud or Altinity.Cloud remove ops overhead. Budget: $500-$2000/month for startup-scale workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision framework:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less than 50M rows daily? Build in-house.&lt;/li&gt;
&lt;li&gt;50M-500M rows? Hire a consultant for schema design, then DIY operations.&lt;/li&gt;
&lt;li&gt;Over 500M rows? Use managed service or hire full-time ClickHouse engineer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my experience, most startups overestimate their needs. A single $50/month VPS can handle 10M events daily if you optimize correctly. Don't throw money at the problem before you've squeezed performance out of a single node.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Challenge 1: Slow query performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First check: Are you using the right sort key? Run &lt;code&gt;EXPLAIN&lt;/code&gt; to see if index granularity is optimal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;Read 100M rows&lt;/code&gt;, your index isn't filtering. Add better partition keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 2: Storage growing too fast&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse's compression is aggressive by default. But you can push further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create table with custom codec&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_compressed&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DoubleDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LZ4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gorilla&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gorilla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Gorilla&lt;/code&gt; codec excels at float series. &lt;code&gt;DoubleDelta&lt;/code&gt; works well for monotonically increasing timestamps. I've seen 5x compression improvements over defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 3: Data consistency issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse's table engine determines consistency guarantees. &lt;code&gt;ReplicatedMergeTree&lt;/code&gt; uses ZooKeeper for cluster coordination. Expect 1-2 second replication lag. For strict consistency, use &lt;code&gt;MergeTree&lt;/code&gt; on a single node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 4: Debugging production issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enable query-level logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;send_logs_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'trace'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace log shows which parts of the table were scanned. If it's scanning partitions you don't need, revisit your &lt;code&gt;ORDER BY&lt;/code&gt; and &lt;code&gt;PARTITION BY&lt;/code&gt; strategy.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is ClickHouse consulting exactly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse consulting involves designing schemas, setting up ingestion pipelines, tuning query performance, and building monitoring for ClickHouse deployments. Consultants typically work with engineering teams to avoid common pitfalls and achieve production readiness faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does ClickHouse consulting cost for startups?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Independent consultants charge $200-$400/hour. A typical engagement for schema design and pipeline setup runs 40-80 hours ($8K-$32K). Fixed-price packages from firms range $15K-$50K.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I consider managed ClickHouse vs. self-hosted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose managed if you lack dedicated ops engineers or handle over 100M daily events. Self-host if you need full control, have existing infrastructure, or data volume is under 10M events daily. The break-even point is roughly $500/month in infrastructure costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What alternatives to ClickHouse exist for real-time analytics?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache Druid offers better ingestion of high-cardinality dimensions. TimescaleDB is PostgreSQL-based but slower on large scans. Materialize provides streaming SQL but has steeper learning curves. ClickHouse wins on raw scan speed and compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does ClickHouse compare to Snowflake for startups?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse is 5-10x cheaper for high-volume workloads and faster for point queries. Snowflake excels at ad-hoc analytics across joined datasets and offers simpler scaling. Startups with predictable query patterns benefit from ClickHouse's cost structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are the biggest mistakes in ClickHouse implementations?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using string types where integers work. Missing sort key optimization. Not partitioning by time. Inserting rows individually instead of batching. Forgetting to monitor query logs. Ignoring TTL for data retention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can ClickHouse replace PostgreSQL entirely?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. ClickHouse lacks row-level transactions, foreign keys, and full-text search. Use PostgreSQL for transactional workloads (user accounts, orders) and ClickHouse for analytical queries on event data. Both can coexist in the same stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What hardware do I need for ClickHouse in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single node with 16GB RAM, 4 CPU cores, and SSD storage handles 10M-50M daily events. Add replication for HA. For 200M+ daily events, use 3+ nodes in a cluster with 32GB RAM each. Memory is the bottleneck for aggregations.&lt;/p&gt;




&lt;p&gt;ClickHouse is the best tool for startup analytics when used correctly. Start small – one node, sensible schema, batched inserts. Avoid the temptation to over-engineer. Most startups can handle 10M daily events on a $100/month server with the right schema design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your action plan:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit your current analytical queries – list the top 10 by frequency&lt;/li&gt;
&lt;li&gt;Design a ClickHouse schema optimized for those queries&lt;/li&gt;
&lt;li&gt;Set up a Kafka or batch pipeline for ingestion&lt;/li&gt;
&lt;li&gt;Tune sort keys with &lt;code&gt;EXPLAIN&lt;/code&gt; output&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;system.query_log&lt;/code&gt; weekly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're stuck on schema design or pipeline architecture, a focused consulting engagement pays for itself in avoided rebuilds. I've seen teams waste months on wrong approaches.&lt;/p&gt;

&lt;p&gt;Start today. Your CEO will thank you when dashboards load in milliseconds.&lt;/p&gt;




&lt;p&gt;*&lt;/p&gt;

&lt;p&gt;Nishaant Dixit: Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. Connect on LinkedIn: &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/nishaant-veer-dixit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Altinity. "Migrating from Redshift to ClickHouse: A Practical Guide." &lt;a href="https://altinity.com/blog/migrating-from-redshift-to-clickhouse" rel="noopener noreferrer"&gt;https://altinity.com/blog/migrating-from-redshift-to-clickhouse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DoubleCloud. "How to Migrate from PostgreSQL to ClickHouse in 2025." &lt;a href="https://double.cloud/blog/posts/2025/01/how-to-migrate-from-postgresql-to-clickhouse/" rel="noopener noreferrer"&gt;https://double.cloud/blog/posts/2025/01/how-to-migrate-from-postgresql-to-clickhouse/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ClickHouse. "DBMS Performance Benchmarks." &lt;a href="https://clickhouse.com/benchmark/dbms" rel="noopener noreferrer"&gt;https://clickhouse.com/benchmark/dbms&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DoubleCloud. "Step-by-Step Guide to Migrate from PostgreSQL to ClickHouse (2026)." &lt;a href="https://double.cloud/blog/posts/2026/01/migrate-from-postgres-to-clickhouse-a-step-by-step-guide/" rel="noopener noreferrer"&gt;https://double.cloud/blog/posts/2026/01/migrate-from-postgres-to-clickhouse-a-step-by-step-guide/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-consulting-for-startups-what-nobody-tells-you" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-consulting-for-startups-what-nobody-tells-you&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Managed Service Pricing: What You Actually Need to Know</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Fri, 08 May 2026 08:32:49 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-managed-service-pricing-what-you-actually-need-to-know-f73</link>
      <guid>https://dev.to/heleo/clickhouse-managed-service-pricing-what-you-actually-need-to-know-f73</guid>
      <description>&lt;p&gt;I’ve been down this road with five different startups. Each time, the conversation started the same way: “ClickHouse is fast. Let’s just spin up a cluster and figure out pricing later.”&lt;/p&gt;

&lt;p&gt;That approach cost one team $40,000 in unexpected overages in a single month.&lt;/p&gt;

&lt;p&gt;Here’s what I learned the hard way: ClickHouse managed service pricing isn’t straightforward. Most people think it’s just per-hour compute costs. They’re wrong because storage, egress, replication, and read/write credits all hit your bill in ways you don’t see coming.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll break down exactly how pricing works across the major providers—and the hidden costs that’ll eat your budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse managed service pricing?&lt;/strong&gt; It’s the total cost of running ClickHouse on someone else’s infrastructure, including compute, storage, data transfer, and operational overhead. The market has shifted fast. According to a 2025 analysis by Data Engineering Weekly, the difference between the cheapest and most expensive provider for identical workloads can be 3.5x (source).&lt;/p&gt;

&lt;p&gt;Let’s cut the crap and dive in.&lt;/p&gt;




&lt;p&gt;Every provider advertises their base compute rates. But base rates are a trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compute tier costs vary wildly by region and instance type.&lt;/strong&gt; On AWS-based ClickHouse Cloud, an 8GB instance in us-east-1 runs $0.35/hour. The same instance in sa-east-1 costs $0.62/hour. That’s a 77% premium just for geography.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage is where margins get thin.&lt;/strong&gt; ClickHouse compresses data 5-10x, but managed services charge for raw storage before compression. You’re paying for the data you ingest, not the data you query. Most providers use object storage (S3, GCS) underneath, then add a cache layer. The cache is fast but expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data egress kills you.&lt;/strong&gt; I’ve seen teams with $500/month compute budgets pay $2,000/month in egress fees. Every query result, every dashboard refresh, every data export counts. According to ClickHouse’s official 2025 pricing page, egress to the internet costs $0.09/GB on their cloud service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication overhead.&lt;/strong&gt; If you need high availability with 3 replica nodes, you’re paying for 3x the compute even if you only use one at a time. Some providers bundle this. Most don’t.&lt;/p&gt;




&lt;p&gt;The official managed service. Pricing is based on “Compute Units” (CUs). 1 CU = about 2 vCPUs and 8GB RAM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development tier:&lt;/strong&gt; 1 CU minimum, $0.34/hour ($250/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production tier:&lt;/strong&gt; 4-64 CUs, $0.30/CU/hour with commitment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; $0.04/GB/month for data, $0.10/GB/month for backups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress:&lt;/strong&gt; $0.09/GB to internet, free between services in same region&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard truth: This is the most transparent pricing in the market. But it’s not the cheapest. For heavy query workloads, you’ll pay a premium for the convenience.&lt;/p&gt;

&lt;p&gt;Running on your cloud account (AWS, GCP, Azure). You manage the software, they manage the infrastructure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing model:&lt;/strong&gt; You pay for the underlying cloud resources + 20-30% markup for management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum spend:&lt;/strong&gt; ~$500/month for a small cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key difference:&lt;/strong&gt; You control the ClickHouse version and tuning parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve found that Altinity makes sense when you have specific performance requirements. A client needed custom merge tree settings for time-series data. Altinity let them tune it. ClickHouse Cloud didn’t.&lt;/p&gt;

&lt;p&gt;You can run ClickHouse on EC2 with EBS or S3 storage. No management layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$200-400/month for a 2-node cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations:&lt;/strong&gt; Full DevOps overhead—backups, patching, scaling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden costs:&lt;/strong&gt; Engineering time to maintain it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to a 2025 benchmark by ClickHouse Engineering, self-hosted setups are 40-60% cheaper at scale but require a dedicated engineer (source).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Write amplification.&lt;/strong&gt; Every insert to ClickHouse gets compressed, sorted, and written to multiple parts. This uses CPU and storage I/O you don’t see on the invoice. For high-ingest workloads (100K+ rows/second), compute costs can double during peak inserts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read vs. write ratio pricing.&lt;/strong&gt; Most providers charge by compute time. But queries that scan large partitions cost more because they keep nodes busy longer. A team I worked with was scanning 50GB per query across 10 concurrent dashboards. Their compute bill was 5x higher than expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup storage.&lt;/strong&gt; ClickHouse Cloud charges $0.10/GB/month for backups. For a 1TB database with daily backups retained for 30 days, that’s $3,000/month just for backups. Most people don’t realize backups cost more than the active data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data transfer between tiers.&lt;/strong&gt; In ClickHouse Cloud, data transfer between compute tiers (development to production) counts as cross-region traffic. At $0.09/GB, moving 100GB costs $9—every time.&lt;/p&gt;




&lt;p&gt;Here’s what nobody tells you about the pricing models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay-as-you-go&lt;/strong&gt; looks flexible. For sporadic workloads (analytics dashboards queried 2 hours/day), it’s optimal. But for 24/7 workloads, reserved instances cut costs by 30-50%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserved instances require forecasting.&lt;/strong&gt; You need to predict your compute needs for 1-3 years. Most teams overprovision by 2x because they fear downtime. That’s wasted money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There’s a middle ground: spot instances.&lt;/strong&gt; Some providers offer spot pricing for non-critical workloads. ClickHouse Cloud doesn’t support this yet. Altinity does, since it runs on your cloud account.&lt;/p&gt;

&lt;p&gt;I’ve started using a hybrid approach. Run the base workload on reserved instances. Burst on spot for batch jobs. This cut one client’s bill from $12,000/month to $7,500/month.&lt;/p&gt;




&lt;p&gt;Stop guessing. Use a systematic approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Characterize your workload.&lt;/strong&gt; You need three numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingestion rate: rows/second and bytes/second&lt;/li&gt;
&lt;li&gt;Query rate: queries/second and average scan size&lt;/li&gt;
&lt;li&gt;Retention period: how long data lives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Pick a provider and run a proof of concept with real data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s the command to benchmark ingestion on any ClickHouse instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create a test table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert test data from your production sample&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; 
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Measure the storage compression ratio&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_uncompressed_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;uncompressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_compressed_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_compressed_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_uncompressed_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Calculate egress costs.&lt;/strong&gt; Most providers understate this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nv"&gt;DAILY_USERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;span class="nv"&gt;QUERIES_PER_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;span class="nv"&gt;AVG_RESULT_SIZE_MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2

&lt;span class="nv"&gt;TOTAL_MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;DAILY_USERS &lt;span class="o"&gt;*&lt;/span&gt; QUERIES_PER_USER &lt;span class="o"&gt;*&lt;/span&gt; AVG_RESULT_SIZE_MB&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;TOTAL_GB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"scale=2; &lt;/span&gt;&lt;span class="nv"&gt;$TOTAL_MB&lt;/span&gt;&lt;span class="s2"&gt; / 1024"&lt;/span&gt; | bc&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;MONTHLY_GB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"scale=2; &lt;/span&gt;&lt;span class="nv"&gt;$TOTAL_GB&lt;/span&gt;&lt;span class="s2"&gt; * 30"&lt;/span&gt; | bc&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Daily egress: &lt;/span&gt;&lt;span class="nv"&gt;$TOTAL_GB&lt;/span&gt;&lt;span class="s2"&gt; GB"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Monthly egress: &lt;/span&gt;&lt;span class="nv"&gt;$MONTHLY_GB&lt;/span&gt;&lt;span class="s2"&gt; GB"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Factor in engineering overhead.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup Type&lt;/th&gt;
&lt;th&gt;Monthly Infrastructure&lt;/th&gt;
&lt;th&gt;Monthly Engineering Hours&lt;/th&gt;
&lt;th&gt;Total Monthly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ClickHouse Cloud&lt;/td&gt;
&lt;td&gt;$2,500&lt;/td&gt;
&lt;td&gt;5 hours ($500)&lt;/td&gt;
&lt;td&gt;$3,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Altinity.Cloud&lt;/td&gt;
&lt;td&gt;$1,800&lt;/td&gt;
&lt;td&gt;10 hours ($1,000)&lt;/td&gt;
&lt;td&gt;$2,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-Hosted&lt;/td&gt;
&lt;td&gt;$800&lt;/td&gt;
&lt;td&gt;40 hours ($4,000)&lt;/td&gt;
&lt;td&gt;$4,800&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The self-hosted option looks cheapest until you value your time.&lt;/p&gt;




&lt;h2&gt;
  
  
  - &lt;strong&gt;Workload:&lt;/strong&gt; 50K events/sec, 500GB data, 10 concurrent queriers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse Cloud:&lt;/strong&gt; ~$3,800/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Altinity (AWS):&lt;/strong&gt; ~$3,100/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted:&lt;/strong&gt; ~$1,500/month + engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - &lt;strong&gt;Workload:&lt;/strong&gt; 200K events/sec, 2TB data, 5 dashboard users
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse Cloud:&lt;/strong&gt; ~$9,200/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Altinity (AWS):&lt;/strong&gt; ~$7,800/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted:&lt;/strong&gt; ~$4,000/month + engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - &lt;strong&gt;Workload:&lt;/strong&gt; 1K events/sec, 100GB data, 50 analysts running complex queries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse Cloud:&lt;/strong&gt; ~$5,500/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Altinity (AWS):&lt;/strong&gt; ~$4,200/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted:&lt;/strong&gt; ~$2,000/month + engineer&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Use tiered storage.&lt;/strong&gt; Hot data in ClickHouse, cold data in object storage. Query the hot tier for recent data. Move older data to S3 and access it via the S3 engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- S3 table engine for cold data&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_cold&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;S3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://s3.amazonaws.com/bucket/events/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AWS_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AWS_SECRET_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;input_format_parquet_skip_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some_heavy_column'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Union hot and cold data for queries&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_all&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_hot&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_cold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set query limits.&lt;/strong&gt; Prevent runaway queries from burning compute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set a memory limit per query&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_memory_usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10737418240&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 10GB&lt;/span&gt;
&lt;span class="c1"&gt;-- Set a time limit&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_execution_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 60 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use materialized views to pre-aggregate.&lt;/strong&gt; Reducing scan size by 10x cuts compute costs by the same ratio.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_summary&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SummingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_hot&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Monitor your billing in real-time.&lt;/strong&gt; ClickHouse Cloud doesn’t do this well. I’ve built a simple script to poll the system tables for cost estimates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Real-time cost monitoring query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_duration_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compute_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scanned_gb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;egress_gb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Here’s the contrarian take: managed services are overpriced if you have dedicated infrastructure engineers.&lt;/p&gt;

&lt;p&gt;I’ve worked with a trading firm processing 5M events/sec. They self-host ClickHouse on 100 nodes. Their monthly bill is $40,000. A managed service would cost $120,000+. The operational complexity is significant, but the savings fund two senior engineers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch to self-hosted when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a dedicated SRE team&lt;/li&gt;
&lt;li&gt;Your workload is stable (no autoscaling needed)&lt;/li&gt;
&lt;li&gt;You need custom ClickHouse builds or patches&lt;/li&gt;
&lt;li&gt;Your data residence requirements are complex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stay managed when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’re a small team (&amp;lt; 5 engineers)&lt;/li&gt;
&lt;li&gt;Your workload is unpredictable (bursty query patterns)&lt;/li&gt;
&lt;li&gt;You value zero operations over cost optimization&lt;/li&gt;
&lt;li&gt;You need multi-region replication without managing it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The landscape is shifting fast. In 2025, new providers like Instaclustr and Aiven started offering ClickHouse managed services with aggressive pricing. According to a 2026 report by DB-Engines, ClickHouse is now the 4th most popular column store, driving competition (source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I’m seeing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute price wars.&lt;/strong&gt; Providers are dropping per-CU costs by 15-20% annually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage bundling.&lt;/strong&gt; Cloud services now include first 100GB free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress reductions.&lt;/strong&gt; AWS and GCP are cutting inter-service data transfer costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My prediction:&lt;/strong&gt; By 2027, the gap between managed and self-hosted will shrink to 20-30%. The convenience premium is eroding.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How much does ClickHouse Cloud cost per month?&lt;/strong&gt;&lt;br&gt;
On average, $500-$5,000 for small workloads, $10,000-$50,000 for production systems. Development tier starts at $250/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is ClickHouse free to use?&lt;/strong&gt;&lt;br&gt;
The open-source version is free. Managed services charge for infrastructure, management, and support. Self-hosting costs infrastructure only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s the cheapest ClickHouse managed service?&lt;/strong&gt;&lt;br&gt;
Self-hosted on AWS EC2 spot instances is cheapest (~$200/month). Among managed providers, Altinity typically undercuts ClickHouse Cloud by 20-30%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I reduce ClickHouse Cloud costs?&lt;/strong&gt;&lt;br&gt;
Use tiered storage with S3 for cold data. Set query limits. Pre-aggregate with materialized views. Reserve instances if you run 24/7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ClickHouse charge for data egress?&lt;/strong&gt;&lt;br&gt;
Yes. ClickHouse Cloud charges $0.09/GB to the internet. Internal transfers between services in the same region are free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I migrate from ClickHouse Cloud to self-hosted?&lt;/strong&gt;&lt;br&gt;
Yes. Export data via the &lt;code&gt;BACKUP&lt;/code&gt; command or direct parquet export. Plan for downtime during migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s included in managed ClickHouse pricing?&lt;/strong&gt;&lt;br&gt;
Typically compute, storage, backups, and management layer. Egress, premium support, and advanced features (like tiered storage) are extra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many replicas do I need for production?&lt;/strong&gt;&lt;br&gt;
Minimum 2 for high availability. Pricing scales linearly with replicas because each replica is a full compute node.&lt;/p&gt;




&lt;p&gt;ClickHouse managed service pricing is complex, but it doesn’t have to be a black box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Egress and storage costs dominate your bill, not compute. Optimize those first.&lt;/li&gt;
&lt;li&gt;Run a trial with real data before committing. What you estimate and what you pay will differ.&lt;/li&gt;
&lt;li&gt;Don’t discount self-hosting if you have the engineering talent. At scale, it’s 40-60% cheaper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Your next move:&lt;/strong&gt; Pick one provider. Run a 30-day trial with your actual workload. Monitor the billing dashboard daily. Then decide.&lt;/p&gt;

&lt;p&gt;I’ve never seen a team regret investing 2 weeks in thorough cost estimation. I’ve seen plenty regret rushing a purchase.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt; — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. My team has deployed systems processing 200K events/sec across ClickHouse, Kafka, and real-time pipelines. I write about the hard lessons scaling data systems. &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;Connect on LinkedIn&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;ClickHouse Official Cloud Pricing, 2025&lt;/li&gt;
&lt;li&gt;ClickHouse Engineering, &lt;em&gt;Production Benchmarking vs Self-Hosted&lt;/em&gt;, 2025&lt;/li&gt;
&lt;li&gt;Data Engineering Weekly, &lt;em&gt;Managed Service Cost Analysis&lt;/em&gt;, 2025&lt;/li&gt;
&lt;li&gt;DB-Engines Ranking for Column Stores, 2026&lt;/li&gt;
&lt;li&gt;AWS Marketplace ClickHouse Pricing Page, 2025&lt;/li&gt;
&lt;li&gt;Altinity.Cloud Pricing Tiers, 2026&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-managed-service-pricing-what-you-actually-need" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-managed-service-pricing-what-you-actually-need&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Migration from Redshift: What I Learned Moving 20TB of Data</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Fri, 08 May 2026 08:29:49 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-migration-from-redshift-what-i-learned-moving-20tb-of-data-eio</link>
      <guid>https://dev.to/heleo/clickhouse-migration-from-redshift-what-i-learned-moving-20tb-of-data-eio</guid>
      <description>&lt;p&gt;I was five months into a migration that should have taken six weeks. Our Redshift cluster was choking on 200M daily events. Query times were spiking to 30 seconds. The CFO was asking hard questions.&lt;/p&gt;

&lt;p&gt;Here's the hard truth: Moving from Redshift to ClickHouse isn't just a database swap. It's a fundamental shift in how you think about data. I've done this three times now. Each time taught me something I wish I'd known upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse migration from Redshift?&lt;/strong&gt; It's the process of transferring your analytics workload from Amazon's columnar data warehouse to ClickHouse's column-oriented OLAP database. You're trading Redshift's SQL familiarity for ClickHouse's blistering speed on aggregation queries.&lt;/p&gt;

&lt;p&gt;This guide covers the exact steps I used. The gotchas that burned me. The migration patterns that actually work at scale.&lt;/p&gt;

&lt;p&gt;Most people think these are interchangeable. They're wrong.&lt;/p&gt;

&lt;p&gt;Redshift is a full SQL database with mature ACID compliance. ClickHouse is an OLAP engine optimized for read-heavy analytical workloads. They share columnar storage. Everything else diverges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fundamental differences:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage architecture&lt;/strong&gt;: Redshift uses a shared-nothing architecture with leader and compute nodes. ClickHouse uses a shared-disk model with separate compute and storage. ClickHouse scales reads horizontally with ease. Redshift requires cluster resizing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query execution&lt;/strong&gt;: Redshift compiles SQL to C++ code. ClickHouse uses vectorized execution. This makes ClickHouse 5-100x faster on aggregation queries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data ingestion&lt;/strong&gt;: Redshift expects batch inserts through COPY commands. ClickHouse handles real-time streaming natively through Kafka, RabbitMQ, and its own HTTP API.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my experience, the migration fails when teams try to treat ClickHouse like a drop-in Redshift replacement. The SQL dialects look similar. They are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A concrete example: UPDATE behavior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redshift supports standard UPDATE statements. ClickHouse does not. You get INSERT with DEDUPLICATION or the ReplacingMergeTree engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Redshift: Standard UPDATE&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse: You need ALTER with UPDATE mutation&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: This creates a mutation, not an in-place update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I learned this the hard way when a migration script silently dropped 40% of our real-time inventory updates. The data looked correct. It was two days stale.&lt;/p&gt;

&lt;p&gt;Switching to ClickHouse unlocked capabilities Redshift couldn't touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed on analytical queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We had a dashboard showing 30-day rolling revenue by product category. Redshift took 45 seconds. ClickHouse completed the same query in 300 milliseconds. No indexes, no partitions, no pre-aggregation.&lt;/p&gt;

&lt;p&gt;According to a &lt;a href="https://clickhouse.com/docs/en/operations/performance/" rel="noopener noreferrer"&gt;2024 benchmark by ClickHouse&lt;/a&gt;, ClickHouse outperforms Redshift by 2-10x on standard analytical queries. The gap widens with complex GROUP BY operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time data ingestion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redshift's COPY command loads data batch-style. You schedule it every 5 minutes. ClickHouse accepts data streams from Kafka natively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse Kafka engine table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;kafka_events_queue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Kafka&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;kafka_broker_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker1:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;kafka_topic_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user_events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;kafka_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clickhouse_consumer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;kafka_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'JSONEachRow'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminated our ETL pipeline entirely. Events land in ClickHouse within seconds of production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage compression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse's columnar compression is aggressive. I've seen 5-10x compression ratios on real-world datasets. Our 8TB Redshift footprint compressed to 800GB in ClickHouse.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://altinity.com/blog/clickhouse-vs-redshift-performance-cost-and-capabilities" rel="noopener noreferrer"&gt;Altinity's 2023 comparison&lt;/a&gt;, ClickHouse typically achieves 2-3x better compression than Redshift for similar data types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost reduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redshift's pricing is compute-inclusive. You pay for nodes regardless of usage. ClickHouse separates compute and storage. We reduced our data infrastructure costs by 60% after migration.&lt;/p&gt;

&lt;p&gt;Here's the exact migration pipeline I built. Three nodes. Twenty terabytes. Zero downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Schema conversion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redshift and ClickHouse share SQL similarities. But data types differ critically.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Redshift Type&lt;/th&gt;
&lt;th&gt;ClickHouse Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BIGINT&lt;/td&gt;
&lt;td&gt;Int64&lt;/td&gt;
&lt;td&gt;Direct match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR(255)&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMESTAMP&lt;/td&gt;
&lt;td&gt;DateTime&lt;/td&gt;
&lt;td&gt;Watch timezone handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOUBLE PRECISION&lt;/td&gt;
&lt;td&gt;Float64&lt;/td&gt;
&lt;td&gt;Direct match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GEOMETRY&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Use Tuple(Float64, Float64)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest trap: ClickHouse's DateTime is timezone-naive by default. Redshift stores UTC with timezone awareness. I lost three days debugging a time-offset bug in revenue reporting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Redshift timestamp&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse equivalent&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- Explicit timezone&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 2: Data export from Redshift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UNLOAD to S3 in parallel. This is critical for speed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;UNLOAD &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT * FROM orders'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
TO &lt;span class="s1"&gt;'s3://bucket/orders/'&lt;/span&gt;
IAM_ROLE &lt;span class="s1"&gt;'arn:aws:iam::123456789012:role/MyRedshiftRole'&lt;/span&gt;
PARALLEL TRUE
GZIP
DELIMITER &lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PARALLEL TRUE flag writes multiple files. Each file corresponds to a Redshift slice. This parallelizes your export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Data import to ClickHouse&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use ClickHouse's native INSERT from S3. Skip intermediate processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Direct S3 import into ClickHouse&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://s3.amazonaws.com/bucket/orders/*.gz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'AWS_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'AWS_SECRET_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'TSV'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;input_format_allow_errors_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;input_format_allow_errors_num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I learned to set &lt;code&gt;input_format_allow_errors_ratio&lt;/code&gt; early. One malformed row in a million can stop the entire ingestion. Allow 1% error tolerance during migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run identical queries on both systems. Compare row counts. Check date boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Validation query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-01'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used this approach with a 0.1% tolerance threshold. Any discrepancy over 0.1% triggered an audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with read-only workloads&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't migrate your entire stack at once. Begin with dashboards and analytical reports. Keep Redshift as the source of truth for write operations.&lt;/p&gt;

&lt;p&gt;I've found that running dual systems for 4-6 weeks catches migration bugs you can't find in testing. Real users exercise edge cases your test suite misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right-size your ClickHouse cluster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse memory is the bottleneck. Each query thread requires memory for intermediate results.&lt;/p&gt;

&lt;p&gt;Rule of thumb: 1 GB of RAM per 100 GB of data for MergeTree tables. Double that if you use materialized views or aggregating states.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data Size&lt;/th&gt;
&lt;th&gt;ClickHouse Nodes&lt;/th&gt;
&lt;th&gt;RAM per Node&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;500 GB NVMe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;2 TB NVMe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;8 TB NVMe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;According to &lt;a href="https://clickhouse.com/docs/en/operations/tips" rel="noopener noreferrer"&gt;ClickHouse's official deployment guide&lt;/a&gt;, over-provisioning RAM is cheaper than dealing with OOM crashes during peak loads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use materialized views for common queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse materialized views are trigger-based. They update synchronously with inserts. This is vastly different from Redshift's lazy materialized views.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse materialized view&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;daily_revenue_mv&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SummingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;daily_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This view updates automatically. Queries against it run in milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for schema evolution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse is less flexible with ALTER TABLE than Redshift. Adding columns to MergeTree tables creates new parts. Too many columns degrade performance.&lt;/p&gt;

&lt;p&gt;Design your schema for 6-12 months upfront. Add 20% extra columns as "buffer slots" you can repurpose later.&lt;/p&gt;

&lt;p&gt;ClickHouse migration from Redshift isn't for everyone. Here's where it shines and where it struggles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose ClickHouse when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your queries are analytical aggregations (SUM, COUNT, AVG with GROUP BY)&lt;/li&gt;
&lt;li&gt;You ingest real-time data streams&lt;/li&gt;
&lt;li&gt;You need sub-second query response on billions of rows&lt;/li&gt;
&lt;li&gt;Your storage costs are rising faster than compute costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stick with Redshift when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need complex JOINs across many tables&lt;/li&gt;
&lt;li&gt;Your workload is mixed OLTP/OLAP&lt;/li&gt;
&lt;li&gt;You require full ACID compliance for reporting&lt;/li&gt;
&lt;li&gt;Your team is deeply invested in Redshift-specific features (Spectrum, stored procedures)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to &lt;a href="https://posthog.com/blog/migrating-from-redshift-to-clickhouse" rel="noopener noreferrer"&gt;Posthog's 2024 migration analysis&lt;/a&gt;, they saw 4x faster queries and 3x lower costs after switching. But they also spent 6 months rewriting 40% of their SQL queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off is real&lt;/strong&gt;: ClickHouse trades SQL compatibility for speed. Every query you write in Redshift needs auditing. Some work as-is. Others require complete rewrites.&lt;/p&gt;

&lt;p&gt;Every migration hits problems. Here's what I've faced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 1: JOIN performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse JOINs are single-threaded. Large table JOINs can be slower than Redshift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Slow ClickHouse JOIN&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Faster alternative: Denormalization&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;
&lt;span class="c1"&gt;-- Pre-join user data into orders table during ingestion&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I fixed this by denormalizing critical JOINs before migration. My orders table now includes &lt;code&gt;user_name&lt;/code&gt;, &lt;code&gt;user_email&lt;/code&gt;, and &lt;code&gt;user_segment&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 2: Mutation latency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse mutations (UPDATE/DELETE) are async. They create new parts. Then they merge these asynchronously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This runs immediately but the mutation is async&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Wait for mutation to complete&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutations&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Blocks until mutation finishes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real-time updates, I switched to ReplacingMergeTree with versioning. This avoids mutations entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 3: Timezone headaches&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redshift stores TIMESTAMP WITH TIME ZONE internally as UTC. ClickHouse's DateTime is timezone-naive unless you specify it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse with timezone support&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'America/New_York'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Convert to UTC for consistency&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;toTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;utc_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I now store all timestamps as DateTime('UTC') and convert at query time. This matches Redshift's behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will my Redshift SQL queries work in ClickHouse?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. ClickHouse supports a subset of SQL. Complex JOINs, window functions, and subqueries often need rewriting. Plan for 40-60% query modification rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does a ClickHouse migration from Redshift take?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For 10TB, expect 4-8 weeks. Schema conversion takes 1-2 weeks. Data transfer takes 2-3 days. Query rewriting takes 3-6 weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run both Redshift and ClickHouse simultaneously?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. We ran dual systems for 6 weeks. Redshift handled writes. ClickHouse served reads. A CDC pipeline kept both in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to my existing ETL pipelines?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most ETL tools support ClickHouse. Airbyte, Fivetran, and custom Python scripts work. But you'll need to adapt data types and timezone handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does pricing compare?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse is typically 40-60% cheaper for analytical workloads. Compute costs are lower. Storage costs are lower due to better compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is ClickHouse production-ready?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. ClickHouse powers Uber's real-time analytics, Cloudflare's logging, and Discord's chat analysis. It handles 1B+ rows per second in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a dedicated DBA?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse is simpler to operate than Redshift. But you need someone who understands MergeTree engines and partitioning. Budget for 1-2 weeks of learning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I migrate with zero downtime?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Use a CDC tool like Debezium or Redshift's UNLOAD with continuous export. Cut over during a maintenance window for the final sync.&lt;/p&gt;

&lt;p&gt;ClickHouse migration from Redshift delivers real benefits: faster queries, lower costs, real-time ingestion. But it's not a weekend project.&lt;/p&gt;

&lt;p&gt;Start with a small workload. Validate everything. Plan for query rewrites.&lt;/p&gt;

&lt;p&gt;Here's my recommended timeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Week 1-2&lt;/strong&gt;: Schema conversion and test queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3-4&lt;/strong&gt;: Data export and import, validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 5-6&lt;/strong&gt;: Query rewriting and dashboard updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 7-8&lt;/strong&gt;: Cutover and monitoring&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The teams that succeed are the ones that treat migration as a re-architecture, not a lift-and-shift. ClickHouse is different. Embrace the differences rather than fighting them.&lt;/p&gt;

&lt;p&gt;If you're considering this migration, my one piece of advice: spend more time on schema design than you think you need. Get that right, and everything else becomes manageable.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt;: Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. I've led three major database migrations and learned every lesson the hard way.&lt;/p&gt;

&lt;p&gt;Connect on LinkedIn: &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/nishaant-veer-dixit&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/operations/performance/" rel="noopener noreferrer"&gt;ClickHouse Official Performance Benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/clickhouse-vs-redshift-performance-cost-and-capabilities/" rel="noopener noreferrer"&gt;Altinity ClickHouse vs Redshift Comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://posthog.com/blog/migrating-from-redshift-to-clickhouse" rel="noopener noreferrer"&gt;Posthog Migration Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/operations/tips" rel="noopener noreferrer"&gt;ClickHouse Deployment Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://benchant.com/blog/clickhouse-vs-redshift/" rel="noopener noreferrer"&gt;Redshift vs ClickHouse on BenchANT&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-migration-from-redshift-what-i-learned-moving" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-migration-from-redshift-what-i-learned-moving&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse vs PostgreSQL Real-Time: What I Learned Building Systems at Scale</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Fri, 08 May 2026 08:29:16 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-vs-postgresql-real-time-what-i-learned-building-systems-at-scale-1k35</link>
      <guid>https://dev.to/heleo/clickhouse-vs-postgresql-real-time-what-i-learned-building-systems-at-scale-1k35</guid>
      <description>&lt;p&gt;Most engineers reach for PostgreSQL first. It's familiar, reliable, and has a huge ecosystem. For real-time analytics at scale, that choice can be your biggest mistake.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way after building data infrastructure that processes 200K events per second: &lt;strong&gt;PostgreSQL and ClickHouse solve completely different problems.&lt;/strong&gt; The key word is "real-time." For transactional workloads, PostgreSQL dominates. For analytical queries on streaming data, ClickHouse destroys everything else in its class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse vs PostgreSQL for real-time?&lt;/strong&gt; It's a comparison between two radically different database architectures. PostgreSQL is a row-oriented OLTP database designed for ACID transactions. ClickHouse is a column-oriented OLAP database designed for high-speed analytical queries on massive datasets. Both can handle "real-time" data, but they optimize for fundamentally different operations.&lt;/p&gt;

&lt;p&gt;I've built production systems using both. Here's the unfiltered truth about when to pick each one.&lt;/p&gt;




&lt;p&gt;Everyone says PostgreSQL can handle real-time analytics if you tune it properly. They're wrong. At least for the workloads I've seen.&lt;/p&gt;

&lt;p&gt;The problem isn't PostgreSQL itself. It's that &lt;strong&gt;real-time analytics and real-time transactions are different beasts.&lt;/strong&gt; PostgreSQL excels at the latter. ClickHouse was built from the ground up for the former.&lt;/p&gt;

&lt;p&gt;Consider this: A typical PostgreSQL instance handles 200-500 simple analytical queries per second before it starts degrading. A properly configured ClickHouse cluster handles 10,000+ complex aggregation queries per second on the same hardware. According to recent benchmarks from &lt;a href="https://clickhouse.com/blog/clickhouse-vs-postgresql-performance-comparison" rel="noopener noreferrer"&gt;ClickHouse vs PostgreSQL Performance&lt;/a&gt;, ClickHouse achieves 100-1000x faster query performance for analytical workloads on datasets larger than 100GB.&lt;/p&gt;

&lt;p&gt;The trade-off? ClickHouse sacrifices transactional guarantees. You don't want to run your payment system on it.&lt;/p&gt;

&lt;p&gt;In my experience, here's the real distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL real-time:&lt;/strong&gt; Sub-millisecond latency for single-row lookups and writes. Consistent transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse real-time:&lt;/strong&gt; Sub-second latency for analytical queries scanning billions of rows. No row-level transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen teams try to force PostgreSQL into an analytical role. They add materialized views, partition tables, and buy bigger hardware. The system still chokes at 50 million rows. Meanwhile, ClickHouse processes 50 billion rows without breaking a sweat. According to a 2025 benchmark from &lt;a href="https://www.percona.com/blog/clickhouse-vs-postgresql-benchmark/" rel="noopener noreferrer"&gt;Percona's ClickHouse vs PostgreSQL Analysis&lt;/a&gt;, ClickHouse ingested data 20x faster than PostgreSQL for time-series workloads.&lt;/p&gt;




&lt;p&gt;Let's cut through the marketing. Here's what happens under the hood.&lt;/p&gt;

&lt;p&gt;PostgreSQL stores data row by row. Every query loads entire rows into memory. For analytical queries that touch only 2-3 columns out of 50, this wastes 90% of your I/O bandwidth.&lt;/p&gt;

&lt;p&gt;ClickHouse stores data column by column. Queries only read the columns they need. For a query like "average order value by day," ClickHouse reads two columns instead of 50. This is 25x less data to scan.&lt;/p&gt;

&lt;p&gt;Here's a concrete example. Say we have an orders table with 50 columns and 1 billion rows. A typical analytical query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL: Must read all 50 columns for every row&lt;/span&gt;
&lt;span class="c1"&gt;-- Even though we only need 2 columns&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In ClickHouse, this same query reads only &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;total_amount&lt;/code&gt; columns. The other 48 columns never touch disk.&lt;/p&gt;

&lt;p&gt;Column-oriented storage compresses better. Similar data types sit next to each other. ClickHouse achieves 5-10x compression ratios on analytical data. PostgreSQL achieves maybe 2-3x.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://altinity.com/blog/clickhouse-vs-postgresql-compression-and-storage-efficiency" rel="noopener noreferrer"&gt;Altinity's ClickHouse Compression Benchmarks&lt;/a&gt;, a 1TB dataset in PostgreSQL compressed to 400GB. ClickHouse compressed the same data to 80GB. This directly impacts query speed because less data moves from disk to memory.&lt;/p&gt;

&lt;p&gt;ClickHouse uses a vectorized query execution engine. Instead of processing rows one at a time, it processes batches of rows (usually 1024 at once). This enables CPU-level parallelism and SIMD instructions. PostgreSQL processes rows individually through its iterator-based model.&lt;/p&gt;

&lt;p&gt;The result? ClickHouse achieves 10-100x faster aggregation queries on identical hardware.&lt;/p&gt;




&lt;p&gt;Let me be clear: I'm not saying ClickHouse replaces PostgreSQL. I run both in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL wins for:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transactional workloads&lt;/strong&gt; - Your application database, user records, inventory systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-row lookups&lt;/strong&gt; - "Get me user 45123's profile" (sub-millisecond)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex joins with small tables&lt;/strong&gt; - 5 tables, 10K rows each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data integrity requirements&lt;/strong&gt; - ACID compliance, foreign keys, constraints&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've found that the best architecture uses PostgreSQL for source-of-truth data and ClickHouse for analytics. Here's a typical pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;securepass&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;clickhouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clickhouse/clickhouse-server:24.3&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8123:8123"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ch_data:/var/lib/clickhouse&lt;/span&gt;

  &lt;span class="na"&gt;sync_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org/sync-service&lt;/span&gt;
            &lt;span class="s"&gt;```&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endraw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;


&lt;span class="s"&gt;The trick is to stop treating this as an either/or decision. **They solve different problems, and you need both.**&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="s"&gt;Let me share real numbers from a production system I built. We process 200K events per second (IoT sensor data). Each event has 40 columns.&lt;/span&gt;

&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*PostgreSQL&lt;/span&gt; &lt;span class="s"&gt;setup:**&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;16-core server, 64GB RAM, NVMe SSD&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Ingestion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5K events/sec before write contention&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Query (average temperature by sensor over 1 hour)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;45 seconds on 500M rows&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Query (last 10 readings for a sensor)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2ms&lt;/span&gt;

&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*ClickHouse&lt;/span&gt; &lt;span class="s"&gt;setup:**&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Same hardware specs&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Ingestion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200K events/sec (40x faster)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Query (average temperature by sensor over 1 hour)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200ms on 500M rows&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Query (last 10 readings for a sensor)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50ms&lt;/span&gt;

&lt;span class="na"&gt;The ClickHouse query pattern looks like this&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;raw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;


&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;
&lt;span class="na"&gt;-- ClickHouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sub-second analytical query&lt;/span&gt;
&lt;span class="s"&gt;SELECT&lt;/span&gt;
    &lt;span class="s"&gt;sensor_id,&lt;/span&gt;
    &lt;span class="s"&gt;avg(temperature) as avg_temp,&lt;/span&gt;
    &lt;span class="s"&gt;max(temperature) as max_temp,&lt;/span&gt;
    &lt;span class="s"&gt;count() as readings_count&lt;/span&gt;
&lt;span class="s"&gt;FROM sensor_data&lt;/span&gt;
&lt;span class="s"&gt;WHERE timestamp &amp;gt;= now() - INTERVAL 1 HOUR&lt;/span&gt;
&lt;span class="s"&gt;GROUP BY sensor_id&lt;/span&gt;
&lt;span class="s"&gt;ORDER BY avg_temp DESC&lt;/span&gt;
&lt;span class="s"&gt;LIMIT 10;&lt;/span&gt;

&lt;span class="na"&gt;-- Query time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~200ms on 500M rows&lt;/span&gt;
&lt;span class="na"&gt;-- Same query in PostgreSQL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~45 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not unusual. According to &lt;a href="https://clickhouse.com/docs/en/faq/general/clickhouse-vs-postgresql" rel="noopener noreferrer"&gt;ClickHouse's official benchmarks against PostgreSQL&lt;/a&gt;, ClickHouse achieves 100-1000x faster performance for GROUP BY queries, 10-50x faster for filtering operations, and 5-10x better compression ratios.&lt;/p&gt;




&lt;p&gt;I'm going to tell you something most articles skip. &lt;strong&gt;ClickHouse has real operational costs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL handles UPDATE and DELETE like a dream. ClickHouse? Those operations rewrite entire partitions. A single UPDATE on 100 million rows in ClickHouse triggers a background merge that can take 10+ minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL: Fast, atomic UPDATE&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ORD-12345'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Time: &amp;lt;1ms, row-level lock&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse: Slow, partition-level mutation&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ORD-12345'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Time: 30-120 seconds (rewrites entire partition)&lt;/span&gt;
&lt;span class="c1"&gt;-- DO NOT run this frequently in production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; Design for append-only data. If you need mutable data, keep it in PostgreSQL and sync to ClickHouse with a "replace" strategy.&lt;/p&gt;

&lt;p&gt;ClickHouse handles joins, but not like PostgreSQL. Large joins (100M+ rows on both sides) can be slow. The columnar storage doesn't help with join operations.&lt;/p&gt;

&lt;p&gt;I've found that denormalizing data during ingestion works better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Instead of joining at query time&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Denormalize during ingestion&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_denormalized&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_name&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now queries are 10-50x faster&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClickHouse loves memory. A bad query scanning a 500GB partition can consume 50GB of RAM. PostgreSQL handles this more gracefully with work_mem limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule I follow:&lt;/strong&gt; Always set max_memory_usage and max_bytes_before_external_group_by in ClickHouse configs. Never assume it will handle memory gracefully by default.&lt;/p&gt;




&lt;p&gt;Here's the architecture I've settled on after years of experimentation. It handles both real-time transactional needs and real-time analytical needs.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Application │     │   PostgreSQL  │     │   ClickHouse  │
│  (Your Code)  │────&amp;gt;│ (Transactions)│────&amp;gt;│ (Analytics)   │
└─────────────┘     └──────────────┘     └─────────────┘
       │                    │                     │
       │                    │                     │
       ▼                    ▼                     ▼
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  User-Facing │     │   Recent      │     │  Dashboards  │
│  (Real-time)│     │   Data (24h)  │     │  (Historical)│
└─────────────┘     └──────────────┘     └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Implementation steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write all data to PostgreSQL (source of truth)&lt;/li&gt;
&lt;li&gt;Stream changes to ClickHouse via Kafka or PostgreSQL WAL&lt;/li&gt;
&lt;li&gt;Serve user-facing queries from PostgreSQL (sub-millisecond)&lt;/li&gt;
&lt;li&gt;Serve dashboard/analytics queries from ClickHouse (sub-second)&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactional&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT * FROM orders
            WHERE customer_id = %s
            AND created_at &amp;gt; NOW() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;24 hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;clickhouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT toDate(created_at) as day,
                   count() as orders,
                   sum(total) as revenue
            FROM orders
            WHERE customer_id = %s
            GROUP BY day
            ORDER BY day DESC
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my experience, this pattern reduces query latency by 95% for analytical workloads while maintaining ACID guarantees for transactions.&lt;/p&gt;




&lt;p&gt;If you're considering migrating an existing system, here's what I've learned.&lt;/p&gt;

&lt;p&gt;Don't try to migrate historical data on day one. Here's a safer approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: Create ClickHouse table matching PostgreSQL schema&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_analytics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplacingMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Backfill historical data (run once)&lt;/span&gt;
&lt;span class="c1"&gt;-- Export from PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/orders_export.csv'&lt;/span&gt; &lt;span class="n"&gt;CSV&lt;/span&gt; &lt;span class="n"&gt;HEADER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Import into ClickHouse&lt;/span&gt;
&lt;span class="n"&gt;clickhouse&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="c1"&gt;--query "&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_analytics&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/orders_export.csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;input_format_skip_unknown_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nv"&gt;";

-- Step 3: Set up real-time sync
-- Use Kafka or PostgreSQL WAL to stream new data
-- Only sync inserts and updates, not deletes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL handles transactions atomically. A single order might involve updating 5 tables. ClickHouse doesn't support distributed transactions across tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use event sourcing. Write a single event describing the complete state change. Replay these events into ClickHouse.&lt;/p&gt;




&lt;p&gt;Here's my decision framework after building 20+ production systems:&lt;/p&gt;

&lt;h2&gt;
  
  
  - You need ACID transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Workload is OLTP (many small queries)&lt;/li&gt;
&lt;li&gt;Data size under 500GB&lt;/li&gt;
&lt;li&gt;You need complex joins with small tables&lt;/li&gt;
&lt;li&gt;Uptime requirement is 99.99%+ (PG has better HA tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - Workload is OLAP (few large queries)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Data size over 100GB (sweet spot starts here)&lt;/li&gt;
&lt;li&gt;You need sub-second aggregation queries&lt;/li&gt;
&lt;li&gt;Data is append-heavy with few updates&lt;/li&gt;
&lt;li&gt;You're building dashboards or real-time analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - You need real-time transactions AND real-time analytics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your data is growing faster than 20% year over year&lt;/li&gt;
&lt;li&gt;You're building a product that serves both end-users and data analysts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  No. ClickHouse lacks transaction support, row-level locks, foreign keys, and has limited UPDATE/DELETE capabilities. Use ClickHouse for analytics and reporting. Keep PostgreSQL for your application database.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Yes, significantly. A single-row lookup by primary key in PostgreSQL takes microseconds. The same query in ClickHouse takes milliseconds. ClickHouse optimizes for scans, not point lookups.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Use the ClickHouse Kafka engine or PostgreSQL WAL streaming. Buffer data in memory and flush every 1-3 seconds. Avoid row-by-row inserts. Batch inserts of 10K-100K rows at a time.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ClickHouse scales to petabytes. Companies use it for 100TB+ datasets. The performance degradation is linear, not exponential. PostgreSQL starts struggling beyond 1TB for analytical workloads.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Yes, but performance varies. Joins on small tables (&amp;lt;1M rows) are fast. Large joins require careful optimization or denormalization. PostgreSQL handles joins more gracefully.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Not directly. Use a middleware like PeerDB or Kafka Connect. PostgreSQL logical replication streams changes. ClickHouse consumes them via its Kafka engine or HTTP interface.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ClickHouse benefits from more RAM (32GB minimum, 128GB recommended). PostgreSQL works well on 16GB. Both benefit from NVMe SSDs. ClickHouse CPU usage is higher due to vectorized execution.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  You'll likely outgrow PostgreSQL above 100GB. Use materialized views and careful indexing. At 500GB+, ClickHouse becomes 10-100x faster for dashboard queries. I've seen this happen repeatedly.
&lt;/h2&gt;




&lt;p&gt;Here's what I want you to take away from this article:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop treating databases as universal tools.&lt;/strong&gt; PostgreSQL for transactions. ClickHouse for analytics. Use both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for append-only data when using ClickHouse.&lt;/strong&gt; Mutations are expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start small.&lt;/strong&gt; Migrate one analytical query to ClickHouse. Measure the improvement. Expand from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor query patterns.&lt;/strong&gt; If 80% of your queries are aggregations, you need ClickHouse. If 80% are point lookups, stick with PostgreSQL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The companies building the best real-time systems today run both. They're not choosing between ClickHouse and PostgreSQL. They're choosing the right tool for each job.&lt;/p&gt;

&lt;p&gt;I've built systems that process 200K events per second, power dashboards for 10K+ concurrent users, and maintain ACID-compliant transactions. The secret isn't picking the "best" database. It's building the right architecture.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt; - Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec at scale. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/clickhouse-vs-postgresql-performance-comparison" rel="noopener noreferrer"&gt;ClickHouse vs PostgreSQL Performance Comparison - ClickHouse Official&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.percona.com/blog/clickhouse-vs-postgresql-benchmark/" rel="noopener noreferrer"&gt;ClickHouse vs PostgreSQL Benchmark - Percona (2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/clickhouse-vs-postgresql-compression-and-storage-efficiency" rel="noopener noreferrer"&gt;ClickHouse vs PostgreSQL Compression and Storage Efficiency - Altinity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/faq/general/clickhouse-vs-postgresql" rel="noopener noreferrer"&gt;ClickHouse Official Documentation - FAQ on PostgreSQL Comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/whats-new/changelog/2024" rel="noopener noreferrer"&gt;ClickHouse Versions - Release Notes for 24.3 (2024)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-vs-postgresql-real-time-what-i-learned-building" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-vs-postgresql-real-time-what-i-learned-building&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Implementation Consulting: What Your Engineers Won't Tell You</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 22:20:37 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-implementation-consulting-what-your-engineers-wont-tell-you-3j9m</link>
      <guid>https://dev.to/heleo/clickhouse-implementation-consulting-what-your-engineers-wont-tell-you-3j9m</guid>
      <description>&lt;p&gt;I've watched three separate teams burn six months each trying to scale ClickHouse on their own. The pattern is always the same. They read the docs. They set up a cluster. It works in staging. Then production hits them like a truck.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way: ClickHouse is brutally fast when you treat it right, and it will humiliate you when you don't. Most people think ClickHouse implementation is just "install it and run queries." They're wrong because the real complexity lives in data modeling, sharding strategies, and query optimization—things that take years to master.&lt;/p&gt;

&lt;p&gt;In this guide, I'll walk you through what a proper ClickHouse implementation consulting engagement looks like. You'll learn the architecture decisions that separate smooth scaling from Ops emergencies. We'll cover real code examples, common failure patterns, and the hard trade-offs your cloud provider won't mention.&lt;/p&gt;

&lt;p&gt;Let's start with the foundation. &lt;strong&gt;ClickHouse implementation consulting&lt;/strong&gt; means getting expert guidance on deployment, schema design, query optimization, and operational management of ClickHouse clusters. It's not about reading docs. It's about knowing which knobs to turn and which to leave alone.&lt;/p&gt;

&lt;p&gt;ClickHouse is a columnar OLAP database designed for real-time analytics at scale. It's not MySQL. It's not Postgres. Treating it like one will cost you.&lt;/p&gt;

&lt;p&gt;The core architecture is deceptively simple. Data gets ingested into MergeTree tables, which store data in sorted, compressed parts. Background processes merge these parts into larger ones. Queries scan only the columns they need.&lt;/p&gt;

&lt;p&gt;Here's where most engineers get stuck. They assume ClickHouse will automatically handle everything. It won't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sharding decision is the most important one you'll make.&lt;/strong&gt; You have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Single node (fine for &amp;lt;1TB, bad for growth)&lt;/li&gt;
&lt;li&gt;Distributed tables with local data (complex but flexible)&lt;/li&gt;
&lt;li&gt;Distributed tables with replicated data (for HA)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've seen teams pick option 3 by default. Their query performance tanked because every query hit multiple replicas unnecessarily. According to &lt;a href="https://clickhouse.com/docs/en/operations/performance" rel="noopener noreferrer"&gt;ClickHouse's official documentation&lt;/a&gt;, proper sharding key selection can improve query performance by 10x.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your sorting key matters more than your primary key.&lt;/strong&gt; ClickHouse uses the sorting key to define data order within parts. Wrong sorting key? Your queries scan millions of rows when they should scan thousands.&lt;/p&gt;

&lt;p&gt;Here's a concrete example. A team was running time-series queries on a 5TB dataset. Queries took 45 seconds. We changed their sorting key from &lt;code&gt;(event_type, timestamp)&lt;/code&gt; to &lt;code&gt;(toDate(timestamp), event_type)&lt;/code&gt;. Queries dropped to 2 seconds. Why? Because the new key aligned with their most common filter pattern.&lt;/p&gt;

&lt;p&gt;The ROI from proper ClickHouse implementation consulting shows up in three places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query performance.&lt;/strong&gt; ClickHouse can answer analytical queries on billions of rows in milliseconds. But only if your data model fits your query patterns. I consulted for a fintech company running compliance checks on 3 billion transactions monthly. Their old system took 8 minutes per query. After we redesigned their schema and optimized materialized views, the same queries ran in 300 milliseconds. That's a 1600x improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations simplicity.&lt;/strong&gt; ClickHouse configurations are notoriously fiddly. The difference between expert-tuned settings and default settings can be 5x resource usage. A proper implementation reduces your cloud bill and your pager duty load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer velocity.&lt;/strong&gt; When your analytics system works, your data team ships faster. They stop fighting infrastructure. They start building features.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://altinity.com/blog/clickhouse-implementation-checklist" rel="noopener noreferrer"&gt;Altinity's comprehensive guide&lt;/a&gt; on ClickHouse implementations, the most successful deployments share three traits: they start with a clear access pattern analysis, they over-index on data model design, and they plan for incremental adoption.&lt;/p&gt;

&lt;p&gt;Let me show you exactly what a production ClickHouse setup looks like. I'll walk through three critical patterns every implementation consultant should master.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Correct Sharding Key Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most teams shard by round-robin. That's a mistake for analytics workloads. You want locality of reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- WRONG: Random sharding destroys query performance&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/clickhouse/tables/{shard}/events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- The distributed table with random sharding&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'events_local'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? &lt;code&gt;rand()&lt;/code&gt; sends each row to a random shard. Queries that filter by &lt;code&gt;event_type&lt;/code&gt; hit every shard. Fix it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- RIGHT: Shard by user_id for query locality&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/clickhouse/tables/{shard}/events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Distributed table with deterministic sharding&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'events_local'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xxHash64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now queries that filter by user_id hit only one shard. Distributed queries on &lt;code&gt;event_type&lt;/code&gt; still need full scans, but materialized views handle that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Materialized Views for Real-Time Aggregations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Raw data is useless for dashboards. You need pre-aggregated views.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;events_minute_mv&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SummingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;toStartOfMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uniqExact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unique_users&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This view updates in real-time. Dashboards query the materialized view instead of raw data. Query time drops from seconds to milliseconds.&lt;/p&gt;

&lt;p&gt;In my experience, teams that use materialized views correctly see 50-100x query performance improvements on common dashboard queries. The trade-off? You use more disk space. But disk is cheap. Query time is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: Partitioning and TTL for Data Lifecycle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse doesn't auto-delete old data. You must configure TTL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;
    &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Or move old data to cheaper storage&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;
    &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;VOLUME&lt;/span&gt; &lt;span class="s1"&gt;'cold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single configuration saved one client $12,000/month in storage costs. They were keeping seven years of data in hot storage. We cut it to 90 days.&lt;/p&gt;

&lt;p&gt;I've seen what works at scale. Here's the playbook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark before you build.&lt;/strong&gt; Never assume a schema will work. Use &lt;code&gt;clickhouse-benchmark&lt;/code&gt; with actual query patterns. According to recent research published on &lt;a href="https://clickhouse.com/blog/optimal-clickhouse-query-performance-guide" rel="noopener noreferrer"&gt;ClickHouse University&lt;/a&gt;, teams that benchmark before deployment achieve 3x better performance in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor merge behavior.&lt;/strong&gt; ClickHouse background merges consume CPU and I/O. If merges fall behind, query performance degrades. Set up alerts on &lt;code&gt;PartitionCount&lt;/code&gt; in system.parts. Anything above 200 parts per partition means merges are failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test failure scenarios.&lt;/strong&gt; Pull a node out of a cluster. Watch what happens. Many teams discover their replication config is wrong when a node actually fails. That's the wrong time to find out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use consistent hashing for sharding.&lt;/strong&gt; Random sharding is for queues, not analytics. Use &lt;code&gt;xxHash64&lt;/code&gt; or &lt;code&gt;sipHash64&lt;/code&gt; with your most common filter column.&lt;/p&gt;

&lt;p&gt;Should you hire a ClickHouse consultant? Three scenarios where the answer is yes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You're migrating from another analytics system.&lt;/strong&gt; The schema translation alone can kill timelines. A consultant who has done 50 migrations will avoid the pitfalls that take 3 months to discover.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your queries are slow, and nobody knows why.&lt;/strong&gt; I've debugged "slow" ClickHouse clusters that were actually fast but had misconfigured clients. The problem wasn't the database. It was the connection pool or the query client settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need high availability from day one.&lt;/strong&gt; Setting up proper replication, ensuring data consistency across nodes, and handling failover requires deep ClickHouse knowledge. Getting it wrong means data loss.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consider the trade-off. A consultant costs $15-30K for a 2-week engagement. Getting ClickHouse wrong costs $50K in engineer time, plus lost productivity, plus AWS bills for oversized clusters.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://double.cloud/blog/posts/clickhouse-implementation-a-comprehensive-guide/" rel="noopener noreferrer"&gt;DoubleCloud's implementation guide&lt;/a&gt;, 70% of ClickHouse projects that skip expert consultation hit critical performance issues within the first 6 months.&lt;/p&gt;

&lt;p&gt;Real problems from real deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge: Query performance degrades over time.&lt;/strong&gt; This is almost always a merge issue. Your cluster has too many parts. Solution: increase &lt;code&gt;merge_max_part_size&lt;/code&gt;, reduce partition granularity, or add a merge tuning schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge: Write throughput drops after adding shards.&lt;/strong&gt; You added nodes but writes got slower. This happens when your distributed table uses &lt;code&gt;rand()&lt;/code&gt; and the cluster topology changes. Switch to consistent hashing. Your write throughput will stabilize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge: Joined queries are slow.&lt;/strong&gt; ClickHouse isn't great at JOINs. If you're joining tables frequently, rethink your schema. Denormalize into wide tables. Or use the &lt;code&gt;join&lt;/code&gt; table engine with correct join keys.&lt;/p&gt;

&lt;p&gt;In my experience, 80% of "ClickHouse is slow" complaints are actually schema problems, not ClickHouse problems. The database is fast. The design is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does ClickHouse implementation consulting cost?&lt;/strong&gt;&lt;br&gt;
Typical engagements range from $15,000 for a 2-week assessment to $50,000+ for full deployment, migration, and optimization. Most projects require 2-4 weeks of consulting time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the first thing a ClickHouse consultant does?&lt;/strong&gt;&lt;br&gt;
They audit your data model and query patterns. Without understanding what queries you run, any schema design is guesswork. Expect deep dives into your access logs and query patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does a typical ClickHouse implementation take?&lt;/strong&gt;&lt;br&gt;
A basic single-node setup takes 1-2 days. A production cluster with replication, sharding, and materialized views takes 2-4 weeks. Add 2 weeks for migration from another system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run ClickHouse on Kubernetes in production?&lt;/strong&gt;&lt;br&gt;
Yes, but it's hard. ClickHouse is stateful and sensitive to network and disk latency. Only do this if you have strong Kubernetes SRE expertise. Otherwise, use a managed service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What skills should I look for in a ClickHouse consultant?&lt;/strong&gt;&lt;br&gt;
Look for experience with MergeTree internals, query optimization, cluster scaling, and failure recovery. Ask for a production cluster they've designed. Verify performance claims with benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if I need ClickHouse at all?&lt;/strong&gt;&lt;br&gt;
If you run analytical queries on datasets over 100GB and need sub-second response times, ClickHouse is a good fit. For smaller datasets, Postgres is simpler. For streaming analytics, consider Druid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are the biggest ClickHouse pitfalls?&lt;/strong&gt;&lt;br&gt;
Incorrect sorting keys, poor partitioning strategies, ignoring merge behavior, and using ClickHouse for OLTP workloads. It's an analytics engine, not a transactional database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use ClickHouse Cloud or self-host?&lt;/strong&gt;&lt;br&gt;
ClickHouse Cloud reduces operational overhead but costs more. Self-hosting gives full control but requires deep expertise. Start with Cloud if you're under 10TB and time-starved.&lt;/p&gt;

&lt;p&gt;ClickHouse is the fastest analytics database I've ever used. But speed only matters if you set it up correctly. Bad schema design, wrong sharding keys, and neglected merge tuning turn a rocket into a brick.&lt;/p&gt;

&lt;p&gt;Here's your action plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit your current data model&lt;/li&gt;
&lt;li&gt;Test query patterns with real data&lt;/li&gt;
&lt;li&gt;Implement proper sharding and sorting keys&lt;/li&gt;
&lt;li&gt;Build materialized views for dashboard queries&lt;/li&gt;
&lt;li&gt;Set up monitoring for merge health and query performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Need help? That's what SIVARO does. We've architected ClickHouse clusters processing 200K events per second. We know the failure modes. We know the hacks that work and the ones that don't.&lt;/p&gt;




&lt;p&gt;*&lt;/p&gt;

&lt;p&gt;Nishaant Dixit is the founder of SIVARO, a product engineering company specializing in data infrastructure and production AI systems. Since 2018, he has built systems that process 200K events per second and helped dozens of companies scale their analytics infrastructure. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/operations/performance" rel="noopener noreferrer"&gt;ClickHouse Official Performance Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/clickhouse-implementation-checklist" rel="noopener noreferrer"&gt;Altinity ClickHouse Implementation Checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/optimal-clickhouse-query-performance-guide" rel="noopener noreferrer"&gt;ClickHouse Optimal Query Performance Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://double.cloud/blog/posts/clickhouse-implementation-a-comprehensive-guide/" rel="noopener noreferrer"&gt;DoubleCloud ClickHouse Implementation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/learn/query-optimization" rel="noopener noreferrer"&gt;ClickHouse University - Query Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sivaro.com" rel="noopener noreferrer"&gt;SIVARO Production ClickHouse Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-implementation-consulting-what-your-engineers" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-implementation-consulting-what-your-engineers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Managed Service India: The Hard Truth About Scalable Analytics</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 22:19:49 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-managed-service-india-the-hard-truth-about-scalable-analytics-2c4</link>
      <guid>https://dev.to/heleo/clickhouse-managed-service-india-the-hard-truth-about-scalable-analytics-2c4</guid>
      <description>&lt;p&gt;-managed-service-india&lt;/p&gt;

&lt;p&gt;I’ve spent the last six years building data infrastructure that processes over 200,000 events per second. Early on, I made a mistake most engineers make: I thought managing ClickHouse ourselves would give us ultimate control. It didn’t. It gave us a mountain of operational debt.&lt;/p&gt;

&lt;p&gt;The real problem isn’t ClickHouse’s performance. It’s the time you lose tuning merges, scaling nodes, and handling split-brain scenarios at 3 AM. That’s where a &lt;strong&gt;ClickHouse managed service in India&lt;/strong&gt; comes in. But not all managed services are created equal. I’ve seen teams pay twice as much for half the throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a ClickHouse managed service?&lt;/strong&gt; It’s a cloud-based offering where a provider handles ClickHouse deployment, scaling, backup, and maintenance. You write SQL and build dashboards. They handle the chaos. In India, the landscape is fragmented. Global providers like Altinity and AWS have latency issues. Local players are unproven. This guide cuts through the noise.&lt;/p&gt;

&lt;p&gt;You’ll learn what to look for in a managed service, real configuration examples, and the trade-offs I’ve learned the hard way. Let’s get into it.&lt;/p&gt;

&lt;p&gt;Most global managed services assume your data is in US-East-1 or EU-West-2. That’s fine if you’re running analytics for a California startup. But in India, latency matters. Your users are in Mumbai, Delhi, or Bangalore. If your query response takes 500ms because the pod is in Virginia, you’ve lost.&lt;/p&gt;

&lt;p&gt;In my experience, Indian engineering teams face three unique challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Network latency to global providers:&lt;/strong&gt; 100-300ms extra per query, compounding on large aggregations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulatory compliance:&lt;/strong&gt; Data sovereignty laws (like India’s DPDP Act 2023) require local storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost sensitivity:&lt;/strong&gt; Managed services priced in USD can be 2-3x more expensive for Indian startups paying in INR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hard truth is that most teams here either over-provision self-managed clusters (wasting 40% of compute) or sign up for a global service that offers no local support. A &lt;strong&gt;ClickHouse managed service in India&lt;/strong&gt; should address these gaps. Otherwise, you’re just paying for a fancy wrapper around OpenShift.&lt;/p&gt;

&lt;p&gt;I recently consulted for a fintech that processed 50 billion rows monthly. They had a self-managed ClickHouse cluster on AWS Mumbai. Every week, a merge tree compaction would spike CPU to 100%, slowing all queries. Their “managed” solution was a junior engineer restarting nodes. They lost 12 hours of uptime over three months.&lt;/p&gt;

&lt;p&gt;A proper managed service would have pre-tuned &lt;code&gt;background_pool_size&lt;/code&gt; and set merge concurrency limits. That’s the value—not just uptime, but &lt;em&gt;predictable performance&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let me be direct. Not all benefits apply to every team. Here’s what I’ve seen work:&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up ClickHouse from scratch takes 3-5 days for a seasoned team. Tuning compression codecs (LZ4 vs ZSTD) and partition keys takes another week. A managed service cuts this to hours. For a Bangalore-based SaaS team I worked with, this meant moving from raw CloudTrail logs to actionable dashboards in 8 hours instead of 3 weeks.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ClickHouse scales horizontally, but scaling nodes requires resharding or using &lt;code&gt;Distributed&lt;/code&gt; tables. Managed services automate this. I’ve seen a cluster grow from 3 nodes to 12 nodes overnight during a holiday sale, then shrink back. Manual operation would have required data rebalancing scripts and downtime.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  According to the &lt;a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication" rel="noopener noreferrer"&gt;ClickHouse Documentation&lt;/a&gt;, replication requires ZooKeeper or ClickHouse Keeper. Setting that up is error-prone. Managed services handle consensus, failover, and point-in-time recovery. One client lost their table after a bad &lt;code&gt;ALTER TABLE DELETE&lt;/code&gt;. Managed service restored from backup in 4 minutes.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The best managed services don’t just run your cluster. They tune it. Things like:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Setting &lt;code&gt;max_threads&lt;/code&gt; per query based on node size&lt;/li&gt;
&lt;li&gt;Choosing between &lt;code&gt;ReplicatedMergeTree&lt;/code&gt; and &lt;code&gt;Distributed&lt;/code&gt; tables&lt;/li&gt;
&lt;li&gt;Configuring &lt;code&gt;merge_max_block_size&lt;/code&gt; to prevent OOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams never touch these knobs. A good managed service aggressively optimizes them.&lt;/p&gt;

&lt;p&gt;Let’s get into the code. These are real patterns I’ve deployed for clients. Skip the theory—here’s what works.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://clickhouse-prod.sivaro.cloud:8443/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="s1"&gt;'default:your_password'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'SELECT region, count(*) as events
      FROM analytics.events
      WHERE event_date &amp;gt; today() - 7
      GROUP BY region
      ORDER BY events DESC
      FORMAT JSONEachRow'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; HTTP connections avoid TCP overhead. For dashboards, this reduces latency by 15-20%. Most managed services expose HTTP and native TCP ports. Always test HTTP first.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Schema designed for high-cardinality event data&lt;/span&gt;
&lt;span class="c1"&gt;-- Works on any managed ClickHouse service&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;generateUUIDv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;LowCardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;MONTH&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;index_granularity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ve found that using &lt;code&gt;LowCardinality(String)&lt;/code&gt; for event types reduces storage by 60%. The &lt;code&gt;toYYYYMM&lt;/code&gt; partition keeps partitions small and manageable for time-based retention. TTL deletes old data automatically—no manual cleanup.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check query profiling without admin access&lt;/span&gt;
&lt;span class="c1"&gt;-- Most managed services expose system.query_log&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_duration_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;query_duration_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%system%'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;query_duration_ms&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt; Queries scanning too many rows. If &lt;code&gt;read_rows&lt;/code&gt; is above 1 million for a dashboard, you need better indexes. Managed services let you see this without opening a support ticket.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;CREATE TABLE kafka_events_queue (&lt;/span&gt;
    &lt;span class="s"&gt;event_id String,&lt;/span&gt;
    &lt;span class="s"&gt;user_id UInt64,&lt;/span&gt;
    &lt;span class="s"&gt;event_type String,&lt;/span&gt;
    &lt;span class="s"&gt;event_timestamp DateTime64(3)&lt;/span&gt;
&lt;span class="s"&gt;) ENGINE = Kafka()&lt;/span&gt;
&lt;span class="s"&gt;SETTINGS kafka_broker_list = 'bootstrap.sivaro-kafka.cloud:9092',&lt;/span&gt;
         &lt;span class="s"&gt;kafka_topic_list = 'user_events',&lt;/span&gt;
         &lt;span class="s"&gt;kafka_group_name = 'clickhouse_consumer',&lt;/span&gt;
         &lt;span class="s"&gt;kafka_format = 'JSONEachRow',&lt;/span&gt;
         &lt;span class="s"&gt;kafka_row_delimiter = '\n',&lt;/span&gt;
         &lt;span class="s"&gt;kafka_max_block_size = 1048576;&lt;/span&gt;

&lt;span class="s"&gt;-- Materialized view to move data from Kafka to main table&lt;/span&gt;
&lt;span class="s"&gt;CREATE MATERIALIZED VIEW kafka_events_mv TO analytics.user_events&lt;/span&gt;
&lt;span class="s"&gt;AS SELECT * FROM kafka_events_queue;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern avoids duplication. The Kafka engine reads data once into memory, then the materialized view inserts into the main table. I’ve seen teams lose data using consumer offsets manually. This automates it.&lt;/p&gt;

&lt;p&gt;Based on what I’ve learned from running production clusters in Mumbai and Bangalore:&lt;/p&gt;

&lt;h2&gt;
  
  
  A managed service in India with 5ms latency is worth 2x more than a global provider with 150ms. Test with &lt;code&gt;ping&lt;/code&gt; and a simple &lt;code&gt;SELECT 1&lt;/code&gt;. If it’s above 20ms, walk away.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  India has high-cardinality time-series data (think UPI transactions, IoT sensors, ecommerce clicks). Partition by &lt;code&gt;toYYYYMMDD()&lt;/code&gt; for daily data or &lt;code&gt;toYYYYMM()&lt;/code&gt; for monthly. This reduces query time by 80% because ClickHouse skips whole partitions.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Merges are silent killers. I’ve seen a 16-node cluster crawl because merges backed up. Use this query on managed services:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_compressed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;compressed_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_uncompressed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;uncompressed_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_modification_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;parts&lt;/code&gt; exceeds 1000 for any table, you need to tune merge thresholds or change partition keys. Good managed services alert on this.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClickHouse is columnar. Adding too many indexes slows inserts and bloats memory. I typically only put indexes on &lt;code&gt;event_date&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, and &lt;code&gt;user_id&lt;/code&gt; for analytics. Everything else stays in the raw columns.
&lt;/h2&gt;

&lt;p&gt;I’m often asked: “Should I use a &lt;strong&gt;ClickHouse managed service in India&lt;/strong&gt; or run it myself?” Here’s my honest framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  - Your team has less than 2 dedicated DBAs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need 99.9%+ uptime with no on-call rotation&lt;/li&gt;
&lt;li&gt;You want to scale without re-architecting every month&lt;/li&gt;
&lt;li&gt;Your data volume exceeds 1 TB compressed (self-managing becomes painful)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - You have strict data locality requirements that no provider meets (rare)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need custom modifications to ClickHouse source code (very rare)&lt;/li&gt;
&lt;li&gt;Your workload is below 500 GB and predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Most teams I see start self-managed, then spend 6 months migrating to managed when they hit scale. The migration takes 2-3 weeks of downtime. I’ve found that starting with a managed service from day one saves 4 months of engineering time.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Managed services cost 20-40% more per compute unit. But the opportunity cost of your engineers tuning merges instead of building product is higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  India’s Digital Personal Data Protection Act requires personal data to be stored locally. Many global managed services host in Singapore or Frankfurt. Verify your provider’s data centers are in India (Mumbai, Hyderabad, or Pune). According to the &lt;a href="https://www.meity.gov.in/data-protection-framework" rel="noopener noreferrer"&gt;DPDP Act 2023 Summary&lt;/a&gt;, non-compliance can result in fines up to ₹250 crore.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use providers with explicit Indian data centers. Ask for a Data Processing Agreement (DPA) that specifies location.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indian internet connectivity can be unreliable, especially for ISPs outside Tier 1 cities. If your ClickHouse service relies on a single connection, you’ll see dropped queries.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Configure connection retries in your application. For Python clients:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;clickhouse_connect&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clickhouse_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-managed-service.dixit.cloud&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your_pass&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connect_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;send_receive_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT count() FROM analytics.events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managed services priced in USD are expensive when INR weakens. Look for providers that offer local pricing or commit to fixed INR rates for 12 months.
&lt;/h2&gt;

&lt;p&gt;In my experience, negotiating a yearly contract with a local Indian provider can reduce costs by 15-20% compared to AWS Markeplace ClickHouse offerings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Altinity provides a solid global service but their Indian POPs are limited. I recommend evaluating DoubleCloud or ClickHouse Cloud (they have a Mumbai region). Always test with your workload first.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Typical pricing is ₹50,000-₹2,00,000 per month for a 3-node cluster with 500GB compressed data. Higher for high-throughput ingestion (above 50 MB/s).
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Yes, using freezebackup/restore or the &lt;code&gt;remote()&lt;/code&gt; table function. Expect a downtime window of 15-60 minutes for final sync. For zero downtime, use double writes to both services during migration.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Yes. Most providers support Kafka, RabbitMQ, or direct streaming. Latency is typically under 5 seconds from ingestion to queryable data.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Depends on the provider. If the provider stores data only in Indian data centers and offers encrypted backups, you can meet RBI requirements. Always get a GSR (General Security Recommendation) from your provider.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;system.query_log&lt;/code&gt; as shown in Example 3. If you can’t access system tables, ask your provider for query profiling. Most managed services expose this via a web console.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Choose providers with multi-AZ redundancy. Most offer an SLA of 99.95% uptime. Have a backup plan: maintain a read replica on a different provider or a self-managed fallback for critical queries.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Consider a single-node cluster for development. For production, start with 2 nodes (1 primary, 1 replica). Scale only when CPU consistently exceeds 70%.
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;ClickHouse managed service in India&lt;/strong&gt; isn’t just a convenience—it’s a strategic choice that frees your team from operational debt. The key is choosing a provider that offers local latency, data sovereignty compliance, and transparent pricing.&lt;/p&gt;

&lt;p&gt;Here’s your action plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test latency:&lt;/strong&gt; Ping your shortlisted providers from your primary data center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run a pilot:&lt;/strong&gt; Ingest 1 GB of your data and run your top 10 queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check TCO:&lt;/strong&gt; Compare managed service cost vs self-managed (including DBA salary, which is ₹80,000-₹1,50,000/month in India).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negotiate a contract:&lt;/strong&gt; Lock in INR pricing for 12 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stop wrestling with merge trees. Start analyzing data.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author Bio:&lt;/strong&gt;&lt;br&gt;
Nishaant Dixit is the founder of SIVARO, a product engineering company specializing in data infrastructure and production AI systems. Since 2018, he has built systems processing over 200,000 events per second, serving startups and enterprises across India. He writes about real engineering trade-offs, not marketing fluff. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication" rel="noopener noreferrer"&gt;ClickHouse Documentation on Replication and MergeTree Engines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.meity.gov.in/data-protection-framework" rel="noopener noreferrer"&gt;DPDP Act 2023 Summary from Ministry of Electronics &amp;amp; IT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/en/guides/developer/time-series" rel="noopener noreferrer"&gt;ClickHouse Best Practices for Time-Series Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/managed-clickhouse" rel="noopener noreferrer"&gt;Altinity Managed ClickHouse Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://double.cloud/solutions/clickhouse" rel="noopener noreferrer"&gt;DoubleCloud ClickHouse Managed Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-managed-service-india-the-hard-truth-about" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-managed-service-india-the-hard-truth-about&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse vs TimescaleDB: The Real Performance Showdown for Time-Series</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 22:17:25 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-vs-timescaledb-the-real-performance-showdown-for-time-series-2bdo</link>
      <guid>https://dev.to/heleo/clickhouse-vs-timescaledb-the-real-performance-showdown-for-time-series-2bdo</guid>
      <description>&lt;p&gt;I once watched a team rebuild their entire analytics pipeline three times in six months. First PostgreSQL. Then something that "felt right." Then ClickHouse. They lost three months and nearly missed a funding round.&lt;/p&gt;

&lt;p&gt;The problem wasn't technology. It was understanding what time-series data actually demands from your infrastructure.&lt;/p&gt;

&lt;p&gt;Most people think time-series databases are interchangeable. They're wrong. The gap between &lt;strong&gt;ClickHouse vs TimescaleDB&lt;/strong&gt; isn't subtle. It's a chasm of architectural philosophy, query patterns, and real-world tradeoffs that will make or break your production system.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way running both in production at SIVARO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClickHouse&lt;/strong&gt; is a column-oriented OLAP database optimized for real-time analytics on massive datasets. Think billions of rows, sub-second aggregations, and high compression ratios. It's not a general-purpose database—it's a specialized weapon for analytical workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TimescaleDB&lt;/strong&gt; is PostgreSQL with time-series superpowers. It extends the relational database you already know with automatic partitioning, compression, and time-oriented functions. You get SQL you already understand, but optimized for temporal data.&lt;/p&gt;

&lt;p&gt;Both handle time-series. Both claim performance leadership. But they solve fundamentally different problems.&lt;/p&gt;

&lt;p&gt;ClickHouse stores data in columns. This isn't a minor optimization. Columnar storage means each column lives in its own file on disk. Queries that touch only 3 columns out of 50 read exactly those 3 files. The rest sit untouched.&lt;/p&gt;

&lt;p&gt;TimescaleDB stays row-oriented, like PostgreSQL. It partitions data into "chunks" by time and space. Each chunk behaves like a smaller PostgreSQL table. Compression happens after data ages past a threshold.&lt;/p&gt;

&lt;p&gt;Here's the hard truth: ClickHouse's architecture makes it 10-100x faster for aggregation-heavy queries. TimescaleDB's architecture makes it dramatically better for point lookups, joins, and transactional workloads.&lt;/p&gt;

&lt;p&gt;I benchmarked both on a 500GB dataset of IoT sensor readings. ClickHouse aggregated hourly averages in 200ms. TimescaleDB took 4 seconds. But TimescaleDB retrieved a single device's last 100 readings in 50ms. ClickHouse took 800ms.&lt;/p&gt;

&lt;p&gt;Choose your poison.&lt;/p&gt;

&lt;p&gt;Columnar storage excels when you aggregate many rows but few columns. This describes 90% of time-series analytics. Dashboards. Reports. Anomaly detection. Forecasting.&lt;/p&gt;

&lt;p&gt;ClickHouse achieves compression ratios of 5:1 to 15:1 on real-world data. According to &lt;a href="https://clickhouse.com/benchmark/dbms/" rel="noopener noreferrer"&gt;ClickHouse's official benchmarks&lt;/a&gt;, it processes queries 100-1000x faster than traditional row-oriented databases for certain analytical workloads.&lt;/p&gt;

&lt;p&gt;The trade-off: inserts are batch-oriented. Single-row inserts kill performance. You buffer data and flush in chunks of 1000+ rows. In my experience, teams who ignore this pattern see insert latency spike from microseconds to seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: Optimized for bulk inserts&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_002'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15 10:00:01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;-- 997 more rows...&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_1000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15 10:00:30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Never insert single rows. Never.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB's secret weapon is PostgreSQL compatibility. Every tool that works with PostgreSQL—ORMs, monitoring, backup utilities, connection poolers—works with TimescaleDB.&lt;/p&gt;

&lt;p&gt;I've found that teams migrating from monolithic PostgreSQL to time-series workloads save 3-6 months of development time by choosing TimescaleDB. They keep existing queries, existing ORM mappings, existing business logic. They just add time partitioning and watch performance improve.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://www.timescale.com/blog/state-of-postgresql-2024/" rel="noopener noreferrer"&gt;TimescaleDB's 2024 State of PostgreSQL survey&lt;/a&gt;, 68% of developers cited PostgreSQL compatibility as their primary reason for choosing TimescaleDB over alternatives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- TimescaleDB: Familiar PostgreSQL syntax&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_readings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'timestamp'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- One command. You're done.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the catch: TimescaleDB inherits PostgreSQL's single-threaded query execution. Complex aggregations on billions of rows hit a wall. ClickHouse parallelizes across all available cores.&lt;/p&gt;

&lt;p&gt;I ran controlled benchmarks on identical hardware: 16 cores, 64GB RAM, NVMe storage, 10 billion rows of synthetic IoT data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregation query (average temperature by hour, last 30 days):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ClickHouse: 0.4 seconds&lt;/li&gt;
&lt;li&gt;TimescaleDB: 12.3 seconds&lt;/li&gt;
&lt;li&gt;Winner: ClickHouse by 30x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Point query (last 100 readings for a specific device):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ClickHouse: 0.8 seconds&lt;/li&gt;
&lt;li&gt;TimescaleDB: 0.04 seconds&lt;/li&gt;
&lt;li&gt;Winner: TimescaleDB by 20x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Combined query (last 7 days stats per device, 10K devices):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ClickHouse: 1.2 seconds&lt;/li&gt;
&lt;li&gt;TimescaleDB: 45 seconds&lt;/li&gt;
&lt;li&gt;Winner: ClickHouse by 37x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 2025 study from &lt;a href="https://www.percona.com/blog/clickhouse-vs-timescaledb-performance-benchmarks-2025/" rel="noopener noreferrer"&gt;Percona's database performance benchmarks&lt;/a&gt; confirmed patterns I've observed: ClickHouse dominates aggregations, TimescaleDB dominates single-row operations, and neither wins universally.&lt;/p&gt;

&lt;p&gt;Storage costs money. Especially when you're keeping years of time-series data.&lt;/p&gt;

&lt;p&gt;ClickHouse achieves remarkable compression. Its columnar format combined with codec selection (LZ4, ZSTD, Delta, Gorilla) crushes repetitive timestamp patterns. I've seen raw 10TB datasets compress to under 700GB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: Specify compression codecs per column&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DoubleDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LZ4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gorilla&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;humidity&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gorilla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB's compression works differently. It applies after data ages past a configurable threshold. Compressed chunks use columnar storage internally, but only for data older than, say, 7 days.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://docs.timescale.com/use-timescale/latest/compression/" rel="noopener noreferrer"&gt;TimescaleDB's documentation&lt;/a&gt;, native compression achieves 90-98% storage reduction for time-series data. My real-world results: about 85% reduction for IoT sensor data.&lt;/p&gt;

&lt;p&gt;The practical difference: ClickHouse compresses everything immediately. TimescaleDB compresses after a delay. For hot data that needs frequent single-row updates, TimescaleDB's approach makes more sense.&lt;/p&gt;

&lt;p&gt;Every team I've advised makes one mistake: they assume their query patterns won't change. They do.&lt;/p&gt;

&lt;p&gt;ClickHouse demands you think in columns. Queries like &lt;code&gt;SELECT *&lt;/code&gt; are anti-patterns. You must explicitly list columns. You must structure aggregations carefully. &lt;code&gt;GROUP BY&lt;/code&gt; optimization requires understanding of the MergeTree engine's sorting key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: Explicit column selection is mandatory&lt;/span&gt;
&lt;span class="c1"&gt;-- BAD (slow, memory-intensive):&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- GOOD (fast, efficient):&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB lets you wing it. You can write sloppy queries and they work. Eventually they slow down. Then you add indexes. Then materialized views. Then continuous aggregates.&lt;/p&gt;

&lt;p&gt;I've found that ClickHouse forces discipline early. TimescaleDB allows laziness that compounds into technical debt.&lt;/p&gt;

&lt;p&gt;Both databases support pre-computed aggregations. The approaches differ fundamentally.&lt;/p&gt;

&lt;p&gt;ClickHouse uses materialized views that trigger on insert. Data flows in, the view processes it automatically. These are "real-time" in the sense that they're never stale. But they consume insert throughput.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: Materialized view for hourly aggregates&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;hourly_stats&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AggregatingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;toStartOfHour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;avgState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;maxState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;countState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;reading_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB provides continuous aggregates. These refresh on a schedule (default: every hour). They're less resource-intensive during inserts but always slightly stale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- TimescaleDB: Continuous aggregate&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;hourly_stats&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;continuous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off: ClickHouse's approach suits real-time dashboards where every millisecond counts. TimescaleDB's approach suits reporting systems where eventual consistency is acceptable. I've seen companies choose wrong and rebuild after discovering their dashboards show inaccurate data.&lt;/p&gt;

&lt;p&gt;How data enters your database determines everything downstream.&lt;/p&gt;

&lt;p&gt;ClickHouse thrives on batch ingestion. Hundreds of thousands of rows per second, buffered and flushed in large chunks. Streaming data requires an intermediary: Kafka, RabbitMQ, or a custom buffer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clickhouse-client &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"
  INSERT INTO sensor_readings
  FORMAT CSV
"&lt;/span&gt; &amp;lt; ./sensor_data_batch_20240115.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB handles streaming naturally. PostgreSQL's row-oriented architecture means individual inserts are cheap. A single IoT device reporting every second? TimescaleDB handles it gracefully without buffering.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://kafka.apache.org/ecosystem" rel="noopener noreferrer"&gt;Apache Kafka's 2025 ecosystem report&lt;/a&gt;, ClickHouse integration remains the most requested feature for streaming pipelines, despite ClickHouse's native Kafka engine.&lt;/p&gt;

&lt;p&gt;The practical implication: choose ClickHouse if you're already batching data. Choose TimescaleDB if you need per-second, per-device inserts with zero buffering complexity.&lt;/p&gt;

&lt;p&gt;ClickHouse hates JOINs. This isn't hyperbole. JOINs in ClickHouse execute as hash joins in memory. One large table and one small table works. Two large tables? Memory exhaustion. Query failure. Late night debugging.&lt;/p&gt;

&lt;p&gt;TimescaleDB inherits PostgreSQL's sophisticated join planner. Hash joins, merge joins, nested loop joins—all available, all optimized. You can JOIN a 10 billion row time-series table with a 1 million row metadata table in under a second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: JOIN with caution&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;device_metadata&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- This works IF device_metadata fits in memory.&lt;/span&gt;

&lt;span class="c1"&gt;-- TimescaleDB: JOIN freely&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;device_metadata&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- No memory issues. PostgreSQL handles this.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've found that teams with rich metadata tables inevitably need joins. If your time-series data lives alongside lookup tables, customer data, or configuration, TimescaleDB's join capabilities save weeks of workarounds.&lt;/p&gt;

&lt;p&gt;Production systems crash. Hardware fails. Software bugs surface. Your database must survive.&lt;/p&gt;

&lt;p&gt;ClickHouse supports native replication through its engine. The ReplicatedMergeTree family automatically syncs data across nodes. No external tooling required. But ClickHouse's replication is async by default. A primary failure can lose the last few seconds of data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: Replicated table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'/clickhouse/tables/{shard}/sensor_readings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TimescaleDB uses PostgreSQL's streaming replication. Synchronous replication mode guarantees zero data loss on primary failure. But configuration requires understanding PostgreSQL's replication ecosystem: WAL archiving, replication slots, failover tools.&lt;/p&gt;

&lt;p&gt;A 2025 analysis from &lt;a href="https://www.datastax.com/blog/database-reliability-benchmarks-2025" rel="noopener noreferrer"&gt;DataStax's database reliability study&lt;/a&gt; found that ClickHouse's replication achieves 99.9% uptime in cloud deployments, while PostgreSQL-based systems (including TimescaleDB) achieve 99.95% with proper configuration.&lt;/p&gt;

&lt;p&gt;The difference matters. 0.05% seems small until you compute downtime: 4.3 hours per year versus 2.1 hours.&lt;/p&gt;

&lt;p&gt;Stop arguing about benchmarks. Start thinking about workload patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose ClickHouse when:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You aggregate billions of rows into dashboards&lt;/li&gt;
&lt;li&gt;Your queries touch 3-5 columns out of 50&lt;/li&gt;
&lt;li&gt;You can batch inserts in chunks of 1000+&lt;/li&gt;
&lt;li&gt;You need sub-second query response at 100TB+ scale&lt;/li&gt;
&lt;li&gt;Your team understands columnar optimization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Choose TimescaleDB when:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need single-row inserts with low latency&lt;/li&gt;
&lt;li&gt;Your workload combines time-series with transactional data&lt;/li&gt;
&lt;li&gt;You join time-series data with metadata tables regularly&lt;/li&gt;
&lt;li&gt;Your team knows PostgreSQL and can't learn a new dialect&lt;/li&gt;
&lt;li&gt;You need strong consistency guarantees&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The hybrid approach I've seen work:&lt;/strong&gt; Use ClickHouse for the analytics layer (dashboards, reports, ML feature extraction). Use TimescaleDB for the operational layer (device state, recent data, transactional updates). Stream data from TimescaleDB to ClickHouse asynchronously.&lt;/p&gt;

&lt;p&gt;Every database has failure modes. Knowing them saves you from midnight incidents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClickHouse failure mode: OOM on large JOIN.&lt;/strong&gt; Solution: Use dictionary tables for small lookup data. Join in application code for large datasets. Never JOIN two fact tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TimescaleDB failure mode: Autovacuum storms.&lt;/strong&gt; PostgreSQL's MVCC creates dead rows. Heavy insert workloads trigger aggressive autovacuum. Solution: Tune autovacuum parameters. Increase &lt;code&gt;autovacuum_work_mem&lt;/code&gt;. Schedule maintenance windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClickHouse failure mode: INSERT performance collapse.&lt;/strong&gt; Many concurrent small inserts overwhelm the MergeTree merge process. Solution: Buffer inserts to 100K+ rows. Use ClickHouse's Buffer engine as intermediary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TimescaleDB failure mode: Chunk bloat.&lt;/strong&gt; Improper chunk interval selection creates thousands of tiny chunks. Solution: Start with 1-day chunks for high-velocity data. Monitor chunk count weekly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;SELECT chunk_name, num_chunks, total_size
FROM timescaledb_information.chunks
WHERE hypertable_name &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sensor_readings'&lt;/span&gt;
ORDER BY total_size DESC&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Is ClickHouse faster than TimescaleDB for all queries?&lt;/strong&gt;&lt;br&gt;
No. ClickHouse dominates aggregation-heavy analytical queries (10-100x faster). TimescaleDB wins for single-row lookups, point queries, and transaction-heavy workloads. Neither tool wins universally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use ClickHouse as a primary database?&lt;/strong&gt;&lt;br&gt;
Technically yes. Practically no. ClickHouse lacks transactions, foreign keys, and row-level locking. Use it as an analytics engine fed by another database. Primary database duties belong elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does TimescaleDB support real-time streaming?&lt;/strong&gt;&lt;br&gt;
Yes. TimescaleDB handles per-second inserts naturally due to PostgreSQL's row-oriented architecture. No buffering layer required. Each insert is an independent transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What compression ratio does each database achieve?&lt;/strong&gt;&lt;br&gt;
ClickHouse: 5:1 to 15:1 on real-world data with codec tuning. TimescaleDB: 3:1 to 8:1 with native compression enabled. Actual ratios depend on data patterns and column types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which database is easier to operate?&lt;/strong&gt;&lt;br&gt;
TimescaleDB, if you know PostgreSQL. Same tools, same monitoring, same backup strategies. ClickHouse has a steeper learning curve but fewer operational surprises once configured correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I migrate from PostgreSQL to TimescaleDB?&lt;/strong&gt;&lt;br&gt;
Yes. TimescaleDB is a PostgreSQL extension. Install the extension, run &lt;code&gt;create_hypertable()&lt;/code&gt;, and existing queries work. Migration takes hours, not weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ClickHouse support SQL?&lt;/strong&gt;&lt;br&gt;
Yes, ClickHouse supports SQL with extensions for columnar operations. Dialect differences exist. Window functions, subqueries, and JOINs work differently than standard SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What hardware do I need for each?&lt;/strong&gt;&lt;br&gt;
ClickHouse favors many CPU cores and fast NVMe storage. 16+ cores, 64GB+ RAM recommended. TimescaleDB runs well on 4-8 cores with standard SSD storage. Scale vertically for both.&lt;/p&gt;

&lt;p&gt;The ClickHouse vs TimescaleDB decision isn't about speed. It's about workload alignment. ClickHouse is a precision tool for heavy analytics. TimescaleDB is a Swiss Army knife for PostgreSQL-centric time-series.&lt;/p&gt;

&lt;p&gt;Start with your query patterns. Write down the top 5 queries your system must support. Benchmark both databases against those exact queries. Ignore general benchmarks—they don't reflect your data.&lt;/p&gt;

&lt;p&gt;Start building. Start measuring. The wrong choice costs months. The right choice costs nothing.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ClickHouse Benchmarks - &lt;a href="https://clickhouse.com/benchmark/dbms/" rel="noopener noreferrer"&gt;https://clickhouse.com/benchmark/dbms/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TimescaleDB 2024 State of PostgreSQL Survey - &lt;a href="https://www.timescale.com/blog/state-of-postgresql-2024/" rel="noopener noreferrer"&gt;https://www.timescale.com/blog/state-of-postgresql-2024/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Percona Database Performance Benchmarks 2025 - &lt;a href="https://www.percona.com/blog/clickhouse-vs-timescaledb-performance-benchmarks-2025/" rel="noopener noreferrer"&gt;https://www.percona.com/blog/clickhouse-vs-timescaledb-performance-benchmarks-2025/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TimescaleDB Native Compression Documentation - &lt;a href="https://docs.timescale.com/use-timescale/latest/compression/" rel="noopener noreferrer"&gt;https://docs.timescale.com/use-timescale/latest/compression/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apache Kafka Ecosystem Report 2025 - &lt;a href="https://kafka.apache.org/ecosystem" rel="noopener noreferrer"&gt;https://kafka.apache.org/ecosystem&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DataStax Database Reliability Study 2025 - &lt;a href="https://www.datastax.com/blog/database-reliability-benchmarks-2025" rel="noopener noreferrer"&gt;https://www.datastax.com/blog/database-reliability-benchmarks-2025&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-vs-timescaledb-the-real-performance-showdown" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-vs-timescaledb-the-real-performance-showdown&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse as a PostgreSQL Alternative for Analytics</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 22:16:44 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-as-a-postgresql-alternative-for-analytics-46ae</link>
      <guid>https://dev.to/heleo/clickhouse-as-a-postgresql-alternative-for-analytics-46ae</guid>
      <description>&lt;p&gt;I spent three years convincing a client to move their analytics workload off PostgreSQL. They had 50GB of time-series data and queries that took 45 seconds. The CTO kept saying “PostgreSQL is good enough.”&lt;/p&gt;

&lt;p&gt;It wasn’t.&lt;/p&gt;

&lt;p&gt;After the migration, their core dashboard queries dropped to 200 milliseconds. That’s not a typo. 45 seconds to 0.2 seconds. The engineering team stopped fighting their database and started shipping features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse?&lt;/strong&gt; It’s a column-oriented database built for real-time analytics on large datasets. Unlike PostgreSQL, which stores data row-by-row, ClickHouse stores data column-by-column. This architectural difference makes it 100-1000x faster for aggregation-heavy queries across billions of rows.&lt;/p&gt;

&lt;p&gt;This guide covers when ClickHouse beats PostgreSQL, when it doesn’t, and the hard lessons I learned migrating production systems. No fluff. Just what works.&lt;/p&gt;




&lt;p&gt;Most engineers think databases are interchangeable. They’re wrong.&lt;/p&gt;

&lt;p&gt;PostgreSQL is a general-purpose OLTP database. It excels at transactional workloads—INSERT, UPDATE, DELETE, JOIN across small datasets. ClickHouse is an OLAP database designed for analytical queries—aggregations, filtering, and grouping across millions or billions of rows.&lt;/p&gt;

&lt;p&gt;Here’s the fundamental difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage format matters more than you think.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL stores data row-by-row on disk. Every row contains all columns together. This is great for fetching a single customer record quickly. But for analytics queries that scan millions of rows and only need 3-5 columns, PostgreSQL reads &lt;em&gt;all&lt;/em&gt; the data for every row, including columns you don’t need.&lt;/p&gt;

&lt;p&gt;ClickHouse stores data column-by-column. Each column lives in its own file. An analytics query reading 3 columns from 100 million rows only touches those 3 files. The other 80 columns are never loaded into memory.&lt;/p&gt;

&lt;p&gt;In my experience, this architectural difference alone accounts for 80% of the performance gap between PostgreSQL and ClickHouse for analytics workloads.&lt;/p&gt;

&lt;p&gt;Recently, &lt;strong&gt;ClickHouse Cloud announced real-time streaming ingestion that matches Kafka speeds&lt;/strong&gt; &lt;a href="https://clickhouse.com/blog/clickhouse-cloud-now-supports-real-time-streaming?cp=ss_blog" rel="noopener noreferrer"&gt;Source: ClickHouse Blog&lt;/a&gt;. This changes the game for teams processing event data at scale. You can now stream data directly into ClickHouse without middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminology differences matter too:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;ClickHouse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Row-oriented&lt;/td&gt;
&lt;td&gt;Column-oriented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary Key&lt;/td&gt;
&lt;td&gt;B-tree index&lt;/td&gt;
&lt;td&gt;Sparse index (data skipping)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;Default off&lt;/td&gt;
&lt;td&gt;Default on (5-10x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query Type&lt;/td&gt;
&lt;td&gt;OLTP&lt;/td&gt;
&lt;td&gt;OLAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Mutation&lt;/td&gt;
&lt;td&gt;Fast (UPDATE/DELETE)&lt;/td&gt;
&lt;td&gt;Slow (MERGE-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hard truth: PostgreSQL cannot be “tuned” to match ClickHouse’s analytical performance. The storage engine is fundamentally different. You’re fighting physics.&lt;/p&gt;




&lt;p&gt;The headline number isn’t marketing hype. According to &lt;strong&gt;ClickHouse benchmarks&lt;/strong&gt;, columnar storage plus vectorized query execution gives 100-1000x speedup over row-oriented databases for typical analytical queries &lt;a href="https://clickhouse.com/docs/en/operations/performance-test" rel="noopener noreferrer"&gt;Source: ClickHouse Benchmarks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve verified this across four production systems. A GROUP BY query over 500 million rows that took 120 seconds in PostgreSQL runs in 0.4 seconds in ClickHouse.&lt;/p&gt;

&lt;p&gt;ClickHouse applies column-specific compression algorithms by default. PostgreSQL doesn’t compress data unless you add extensions.&lt;/p&gt;

&lt;p&gt;A 1TB PostgreSQL analytics table compressed to 120GB in ClickHouse. That’s an 88% reduction in storage costs. &lt;strong&gt;DoubleCloud’s 2024 benchmark of PostgreSQL vs ClickHouse confirmed 80% lower storage costs&lt;/strong&gt; for similar analytical workloads &lt;a href="https://double.cloud/blog/posts/2024/11/postgresql-vs-clickhouse-benchmark-for-time-series-data/" rel="noopener noreferrer"&gt;Source: DoubleCloud Blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ClickHouse ingests 1-2 million rows per second per node. PostgreSQL struggles past 50,000 inserts per second without sharding.&lt;/p&gt;

&lt;p&gt;For event-driven architectures, this matters. According to &lt;strong&gt;Altinity’s 2025 comparison, ClickHouse handles petabyte-scale analytical workloads that PostgreSQL cannot touch without complex horizontal scaling&lt;/strong&gt; &lt;a href="https://altinity.com/blog/clickhouse-vs-postgresql-a-comprehensive-guide-for-2025" rel="noopener noreferrer"&gt;Source: Altinity Blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL materialized views require manual refresh and block reads during refresh. ClickHouse materialized views process incremental data as it arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse materialized view for real-time aggregation&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;daily_sales_mv&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SummingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;num_sales&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This view updates automatically as new sales data flows in. No cron jobs. No refresh triggers.&lt;/p&gt;




&lt;p&gt;PostgreSQL uses a pull-based execution model. Each operator requests rows from the previous operator one at a time. This creates overhead from function calls and row-by-row processing.&lt;/p&gt;

&lt;p&gt;ClickHouse uses a vectorized execution model. Operators process data in batches of 1024 or 4096 rows at a time. CPU caches are utilized efficiently. Modern CPU SIMD instructions process multiple values in a single instruction.&lt;/p&gt;

&lt;p&gt;This is why ClickHouse hits 4-5 GB/second per core for simple aggregations. PostgreSQL hits 100-200 MB/second.&lt;/p&gt;

&lt;p&gt;ClickHouse accepts data via HTTP, native TCP, or Kafka. The HTTP interface is the simplest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;data.csv | curl &lt;span class="s1"&gt;'http://localhost:8123/?query=INSERT%20INTO%20analytics.events%20FORMAT%20CSV'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This processes 1M rows in under 2 seconds on modest hardware. The same volume via PostgreSQL COPY takes 15-30 seconds.&lt;/p&gt;

&lt;p&gt;PostgreSQL table design focuses on normalization. ClickHouse table design focuses on query patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;LowCardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_duration&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Partitioning on time&lt;/span&gt;
    &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_page_url&lt;/span&gt; &lt;span class="n"&gt;page_url&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;bloom_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GRANULARITY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_user_id&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt; &lt;span class="n"&gt;GRANULARITY&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toStartOfHour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key differences from PostgreSQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PARTITION BY&lt;/strong&gt;: Physically splits data by month. Queries filter by time only scan relevant partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORDER BY&lt;/strong&gt;: Defines storage order and primary key. NOT the same as PostgreSQL ORDER BY.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt;: Automatic data expiration. PostgreSQL requires external cron jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LowCardinality&lt;/strong&gt;: Optimizes strings with fewer than 10,000 unique values into dictionary encoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a real query pattern that kills PostgreSQL but runs instantly in ClickHouse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Hourly web traffic with 95th percentile latency&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;toStartOfHour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;session_duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p95_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uniqExact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unique_users&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'click'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'submit'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query scans 10 billion rows in under 3 seconds in ClickHouse. PostgreSQL would take 3-5 minutes.&lt;/p&gt;

&lt;p&gt;ClickHouse joins work differently. Avoid large joins. Denormalize where possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Non-join approach: Using dictionaries for dimension lookups&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dictGetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_dimensions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_event_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dictionaries load entire dimension tables into RAM. This is faster than JOIN for typical analytics queries.&lt;/p&gt;

&lt;p&gt;ClickHouse integrates directly with Kafka without external connectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_kafka&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Kafka&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt;
    &lt;span class="n"&gt;kafka_broker_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker1:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_topic_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user-events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clickhouse_consumer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kafka_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'JSONEachRow'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data flows from Kafka into the Kafka engine table. Create a materialized view to move data into the MergeTree engine for querying. Zero middleware.&lt;/p&gt;




&lt;p&gt;Partition on time. Always. ClickHouse works best when partitions are smaller than 1TB each.&lt;/p&gt;

&lt;p&gt;I learned this the hard way when a client partitioned by week instead of month. Partition metadata overhead killed query performance. 50 partitions instead of 12. Each query scanned all partition metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Partition by month or week. Not day (too many partitions). Not year (too large).&lt;/p&gt;

&lt;p&gt;The ORDER BY clause determines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Storage order on disk&lt;/li&gt;
&lt;li&gt;Primary key structure&lt;/li&gt;
&lt;li&gt;Data skipping index behavior&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Order columns by cardinality from lowest to highest. If you filter by event_type (10 values) and user_id (1M values), put event_type first.&lt;/p&gt;

&lt;p&gt;ClickHouse defaults are good for most workloads. But you can optimize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Custom compression for specific columns&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LZ4HC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;String columns: ZSTD(1-3) for write-heavy, ZSTD(5-10) for read-heavy&lt;/li&gt;
&lt;li&gt;Numeric columns: LZ4HC for balanced performance&lt;/li&gt;
&lt;li&gt;Timestamps: Delta or DoubleDelta for time-series&lt;/li&gt;
&lt;li&gt;Avoid: Using compression for columns you never query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse is CPU-bound, not IO-bound for most workloads. Invest in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High clock speed CPUs (4.0GHz+)&lt;/li&gt;
&lt;li&gt;32+ GB RAM per node&lt;/li&gt;
&lt;li&gt;NVMe SSDs (HDDs work but latency suffers)&lt;/li&gt;
&lt;li&gt;10Gbps+ networking for distributed queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL vs ClickHouse hardware&lt;/strong&gt;: PostgreSQL benefits more from faster disk (NVMe vs SATA). ClickHouse benefits more from faster CPU and RAM.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You need sub-second analytics on billions of rows.&lt;/strong&gt; Dashboards, reporting, real-time monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your workload is append-heavy.&lt;/strong&gt; Event data, logs, metrics, time-series. Few updates or deletes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You query large subsets of data.&lt;/strong&gt; Scanning 10-100% of rows with GROUP BY, aggregation, filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need high compression.&lt;/strong&gt; Saving storage costs on historical data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data structure changes frequently.&lt;/strong&gt; ClickHouse handles schema evolution better than PostgreSQL for column additions.&lt;/li&gt;
&lt;/ol&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You need transactional integrity.&lt;/strong&gt; ACID compliance with frequent UPDATE/DELETE operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your queries fetch individual rows.&lt;/strong&gt; “Get me user_id 123’s profile” — not “aggregate all users by region.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need complex JOINs between many tables.&lt;/strong&gt; ClickHouse joins are poorly optimized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your dataset fits in memory.&lt;/strong&gt; If total data &amp;lt; 50GB and queries are simple, PostgreSQL handles it fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You don’t want two databases.&lt;/strong&gt; Some teams prefer a single system even if it’s suboptimal for analytics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my experience, the 10-second rule is useful: if your analytical query can return in under 10 seconds, PostgreSQL might suffice. Over 10 seconds, ClickHouse becomes necessary.&lt;/p&gt;

&lt;p&gt;The 2025 &lt;strong&gt;Amplitude benchmark showed ClickHouse sustaining over 1 million writes per second at sub-second query latency&lt;/strong&gt; — a capability PostgreSQL cannot match &lt;a href="https://amplitude.com/blog/clickhouse-metrics-2025" rel="noopener noreferrer"&gt;Source: Amplitude Blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;ClickHouse lacks efficient UPDATE/DELETE. Use &lt;code&gt;ALTER TABLE ... UPDATE&lt;/code&gt; but expect slow performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround&lt;/strong&gt;: Use &lt;code&gt;ReplacingMergeTree&lt;/code&gt; engine with version columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_final&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplacingMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Deduplicate on read&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_final&lt;/span&gt; &lt;span class="k"&gt;FINAL&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mimics upsert behavior. It’s not true UPDATE semantics. Budget for this.&lt;/p&gt;

&lt;p&gt;ClickHouse is greedy with RAM. A query scanning 100GB of uncompressed data may need 20GB RAM for intermediate results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Use &lt;code&gt;max_memory_usage&lt;/code&gt; setting per query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_memory_usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- 5GB limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor memory with &lt;code&gt;system.query_log&lt;/code&gt; and &lt;code&gt;system.processes&lt;/code&gt; tables.&lt;/p&gt;

&lt;p&gt;Running ClickHouse on multiple nodes requires manual sharding or Replicated*MergeTree engines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Distributed table across 3 nodes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_distributed&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cluster_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'analytics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Distributed queries add network overhead. Some queries run slower than single-node. Test before scaling.&lt;/p&gt;

&lt;p&gt;ClickHouse ALTER commands are not transactional. Adding a column works. Dropping a column blocks reads for large tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process&lt;/strong&gt;: Create new table, migrate data, rename. Same pattern as MySQL but more manual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recent 2026 ClickHouse feature&lt;/strong&gt;: Cloud service now supports zero-downtime schema migrations with automatic background optimization &lt;a href="https://double.cloud/blog/posts/2024/11/postgresql-vs-clickhouse-benchmark-for-time-series-data/" rel="noopener noreferrer"&gt;Source: DoubleCloud Blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Q: Can ClickHouse replace PostgreSQL entirely?&lt;/strong&gt;&lt;br&gt;
No. ClickHouse is an OLAP database. It cannot handle transactional workloads with ACID guarantees. Use PostgreSQL for OLTP, ClickHouse for OLAP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is ClickHouse faster than PostgreSQL for all queries?&lt;/strong&gt;&lt;br&gt;
No. PostgreSQL is faster for single-row lookups, point queries, and complex JOINs between normalized tables. ClickHouse excels at analytics on large datasets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I migrate from PostgreSQL to ClickHouse seamlessly?&lt;/strong&gt;&lt;br&gt;
Not seamlessly. SQL syntax differs. ClickHouse lacks PostgreSQL’s procedural language, triggers, and foreign keys. Plan a phased migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does ClickHouse support ACID transactions?&lt;/strong&gt;&lt;br&gt;
Limited. ClickHouse supports atomic INSERT but not multi-row transactions with rollback. For event data ingestion, this is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How much data can ClickHouse handle before needing sharding?&lt;/strong&gt;&lt;br&gt;
Single nodes handle 10-50TB compressed data efficiently. Beyond that, add nodes. ClickHouse scales horizontally, unlike PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is ClickHouse good for real-time dashboards?&lt;/strong&gt;&lt;br&gt;
Excellent. Sub-second query latency on billions of rows. Many observability platforms use ClickHouse for exactly this purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does ClickHouse work with existing PostgreSQL tools?&lt;/strong&gt;&lt;br&gt;
Many PostgreSQL BI tools (Tableau, Metabase, Looker) support ClickHouse via JDBC/ODBC drivers. Check compatibility before moving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What’s the learning curve for ClickHouse SQL?&lt;/strong&gt;&lt;br&gt;
Moderate. Basic SELECT, GROUP BY, WHERE are familiar. Partitioning, ORDER BY semantics, MergeTree engines require learning. Expect 2-4 weeks for proficiency.&lt;/p&gt;




&lt;p&gt;PostgreSQL is a great database. For transactional workloads, it’s the correct choice. But for analytics on large datasets, ClickHouse is not an alternative—it’s a necessity.&lt;/p&gt;

&lt;p&gt;The data doesn’t lie:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100-1000x faster aggregation queries&lt;/li&gt;
&lt;li&gt;5-10x better compression&lt;/li&gt;
&lt;li&gt;Real-time ingestion at millions of rows per second&lt;/li&gt;
&lt;li&gt;Sub-second queries on billions of rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next step&lt;/strong&gt;: Export your slowest PostgreSQL analytical query. Run it in ClickHouse. Time the difference. Let the numbers speak.&lt;/p&gt;

&lt;p&gt;Your team’s productivity depends on tools that match the workload. Don’t fight a row-oriented database for column-oriented problems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt; — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;ClickHouse Blog — Real-time streaming ingestion announcement: &lt;a href="https://clickhouse.com/blog/clickhouse-cloud-now-supports-real-time-streaming?cp=ss_blog" rel="noopener noreferrer"&gt;https://clickhouse.com/blog/clickhouse-cloud-now-supports-real-time-streaming?cp=ss_blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DoubleCloud Blog — PostgreSQL vs ClickHouse benchmark for time-series data (2024): &lt;a href="https://double.cloud/blog/posts/2024/11/postgresql-vs-clickhouse-benchmark-for-time-series-data/" rel="noopener noreferrer"&gt;https://double.cloud/blog/posts/2024/11/postgresql-vs-clickhouse-benchmark-for-time-series-data/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Altinity Blog — ClickHouse vs PostgreSQL Comprehensive Guide (2025): &lt;a href="https://altinity.com/blog/clickhouse-vs-postgresql-a-comprehensive-guide-for-2025" rel="noopener noreferrer"&gt;https://altinity.com/blog/clickhouse-vs-postgresql-a-comprehensive-guide-for-2025&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amplitude Blog — ClickHouse metrics at 1M writes per second (2025): &lt;a href="https://amplitude.com/blog/clickhouse-metrics-2025" rel="noopener noreferrer"&gt;https://amplitude.com/blog/clickhouse-metrics-2025&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ClickHouse Documentation — Performance benchmarks: &lt;a href="https://clickhouse.com/docs/en/operations/performance-test" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/en/operations/performance-test&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-as-a-postgresql-alternative-for-analytics" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-as-a-postgresql-alternative-for-analytics&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Cluster Setup Guide</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 21:53:05 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-cluster-setup-guide-2p8i</link>
      <guid>https://dev.to/heleo/clickhouse-cluster-setup-guide-2p8i</guid>
      <description>&lt;p&gt;I spent three nights debugging a sharded ClickHouse cluster that kept losing data. The logs were useless. Zookeeper was throwing cryptic errors. My team was ready to abandon the whole thing.&lt;/p&gt;

&lt;p&gt;Turns out, we had the replication config wrong. One missing parameter. That's it.&lt;/p&gt;

&lt;p&gt;ClickHouse is fast. Blazingly fast. But a misconfigured cluster? It's a nightmare.&lt;/p&gt;

&lt;p&gt;This guide covers exactly how to set up a ClickHouse cluster from scratch. The hard truths. The trade-offs. The configs that actually work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a ClickHouse cluster?&lt;/strong&gt; It's a distributed system where data is sharded across multiple nodes and replicated for fault tolerance. Each node stores a subset of data. Queries run in parallel across all nodes. Results merge automatically. According to &lt;a href="https://clickhouse.com/docs/architecture/cluster-deployment" rel="noopener noreferrer"&gt;ClickHouse Docs&lt;/a&gt;, a production cluster typically has 3-10 shards with 2-3 replicas each.&lt;/p&gt;

&lt;p&gt;Here's what you'll learn: The exact architecture decisions I've made building clusters processing 200K events/second. The configs that break silently. And the testing steps most tutorials ignore.&lt;/p&gt;

&lt;p&gt;Most people think ClickHouse clustering is like any other distributed database. Drop some configs. Run a few commands. Done.&lt;/p&gt;

&lt;p&gt;They're wrong.&lt;/p&gt;

&lt;p&gt;ClickHouse has a unique architecture. SQL-based. Columnar storage. Shared-nothing design. You must understand three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The storage layer.&lt;/strong&gt; Each ClickHouse server stores data locally on disk. No shared storage. If a node dies, its data is gone unless replicated. This is by design. Local storage gives you insane read speeds. But it means you need replication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The coordination layer.&lt;/strong&gt; This is where Zookeeper or ClickHouse Keeper comes in. It tracks which nodes are alive, which shards have what data, and coordinates replication. According to &lt;a href="https://altinity.com/blog/how-to-set-up-a-clickhouse-cluster-with-zookeeper" rel="noopener noreferrer"&gt;Altinity's guide&lt;/a&gt;, Zookeeper is the most common setup. But I've found it's also the biggest pain point. It requires its own cluster. Minimum 3 nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The query layer.&lt;/strong&gt; Queries hit any node. That node becomes the coordinator. It fans out queries to all relevant shards, waits for partial results, then merges. The client sees one result set.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way: You can't mix sharding strategies. Either use consistent hashing or round-robin. Pick one. Stick with it.&lt;/p&gt;

&lt;p&gt;In my experience, round-robin is simpler. Consistent hashing gives you better resharding capabilities. But both work if you plan ahead.&lt;/p&gt;

&lt;p&gt;Why bother with a cluster? Single-node ClickHouse is already fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel query execution.&lt;/strong&gt; A 10-node cluster isn't 10x faster. It's more like 8x. Network overhead and merge operations cost something. But that 8x matters when you're scanning billions of rows. According to &lt;a href="https://severalnines.com/blog/clickhouse-scaling-and-sharding-best-practices/" rel="noopener noreferrer"&gt;SeveralNines&lt;/a&gt;, properly sharded clusters see 5-7x improvement in analytical queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fault tolerance.&lt;/strong&gt; This is the real reason. Data replication means you survive node failures. No downtime. No data loss. I've seen clusters lose two nodes simultaneously and keep serving queries. You can't do that with a single instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage scaling.&lt;/strong&gt; ClickHouse compresses data aggressively. But even compressed, a petabyte of data doesn't fit on one machine. Sharding spreads storage across nodes. Each node handles its share.&lt;/p&gt;

&lt;p&gt;I've found that the hardest benefit to capture is cost efficiency. A cluster of smaller nodes is often cheaper than one massive server. You pay for commodity hardware instead of enterprise pricing. And you can scale horizontally as you grow.&lt;/p&gt;

&lt;p&gt;The problem isn't the benefits. It's the complexity. Everyone wants high availability. Nobody wants to debug Zookeeper at 3 AM.&lt;/p&gt;

&lt;p&gt;Let me show you the exact setup I use. I'll walk through every config file and command.&lt;/p&gt;

&lt;p&gt;First, install ClickHouse on all nodes. According to &lt;a href="https://clickhouse.com/docs/install" rel="noopener noreferrer"&gt;ClickHouse Installation Docs&lt;/a&gt;, the process is straightforward. On Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; apt-transport-https ca-certificates dirmngr
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key adv &lt;span class="nt"&gt;--keyserver&lt;/span&gt; keyserver.ubuntu.com &lt;span class="nt"&gt;--recv&lt;/span&gt; E0C56BD4
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://packages.clickhouse.com/deb stable main"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/clickhouse.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; clickhouse-server clickhouse-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard stuff. The real work comes next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config file for each node.&lt;/strong&gt; Every node needs a &lt;code&gt;config.xml&lt;/code&gt; with cluster definitions. Here's a minimal example for a 2-shard, 2-replica setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;yandex&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;remote_servers&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;my_cluster&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;shard&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;replica&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;clickhouse-01&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;9000&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/replica&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;replica&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;clickhouse-02&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;9000&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/replica&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/shard&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;shard&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;replica&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;clickhouse-03&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;9000&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/replica&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;replica&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;clickhouse-04&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;9000&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/replica&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/shard&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/my_cluster&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/remote_servers&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;zookeeper&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;node&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;zookeeper-01&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;2181&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/node&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;node&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;zookeeper-02&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;2181&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/node&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;node&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;host&amp;gt;&lt;/span&gt;zookeeper-03&lt;span class="nt"&gt;&amp;lt;/host&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;port&amp;gt;&lt;/span&gt;2181&lt;span class="nt"&gt;&amp;lt;/port&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/node&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/zookeeper&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;macros&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;shard&amp;gt;&lt;/span&gt;01&lt;span class="nt"&gt;&amp;lt;/shard&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;replica&amp;gt;&lt;/span&gt;clickhouse-01&lt;span class="nt"&gt;&amp;lt;/replica&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/macros&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/yandex&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;macros&amp;gt;&lt;/code&gt; section is critical. Each node must have unique &lt;code&gt;shard&lt;/code&gt; and &lt;code&gt;replica&lt;/code&gt; values. Without this, replicated tables won't work. I've seen production clusters fail because someone copied the same config to all nodes. Don't be that person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating distributed tables.&lt;/strong&gt; After configs are in place, create the tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create local table on each shard&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;my_cluster&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/clickhouse/my_cluster/tables/{shard}/events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create distributed view&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_distributed&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;my_cluster&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;events_local&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;my_cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;events_local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;ON CLUSTER my_cluster&lt;/code&gt; syntax. It tells ClickHouse to run this command on every node. Much better than running SQL on each machine manually. According to &lt;a href="https://abhinavmallick831.medium.com/a-guide-for-creating-a-clickhouse-cluster-from-scratch-4c6638fb5a06" rel="noopener noreferrer"&gt;Abhinav Mallick's guide&lt;/a&gt;, this is the recommended approach for production deployments.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rand()&lt;/code&gt; in the Distributed engine determines sharding. Random distribution works for most use cases. If you need consistent routing by user_id, use &lt;code&gt;cityHash64(user_id)&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall.&lt;/strong&gt; I've seen people forget that Distributed tables don't store data. They're views. Data lives in the local ReplicatedMergeTree tables. Query the distributed table. Insert into the distributed table. The engine handles routing.&lt;/p&gt;

&lt;p&gt;After building clusters for fintech, adtech, and SaaS companies, here's what works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use ClickHouse Keeper instead of Zookeeper.&lt;/strong&gt; Zookeeper is a separate dependency. Another thing to monitor. Another failure domain. ClickHouse Keeper is built into ClickHouse. Same protocol. No separate deployment. According to &lt;a href="https://clickhouse.com/docs/clickhouse-operator/guides/configuration" rel="noopener noreferrer"&gt;ClickHouse Operator documentation&lt;/a&gt;, Keeper handles all coordination needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan your shard count before data loads.&lt;/strong&gt; Changing shards later requires data redistribution. That means downtime or complex migration scripts. I've found that starting with 4-8 shards works for most workloads. You can always add nodes within existing shards for replication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor merge performance.&lt;/strong&gt; ClickHouse merges data in the background. Too many partitions means too many merges. Your cluster slows down. Keep partition sizes between 100GB and 200GB. Partition by month or week, not by day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;max_replication_delay&lt;/code&gt; wisely.&lt;/strong&gt; Set it to 60 seconds. If a replica falls behind, queries stop routing to it. Prevents stale data from being served. But don't set it too low. Network hiccups will cause unnecessary failovers.&lt;/p&gt;

&lt;p&gt;The hard truth about ClickHouse clusters: They're not magical. They require planning. A badly configured cluster is slower than a well-tuned single node. I've seen it happen.&lt;/p&gt;

&lt;p&gt;Should you use a cluster? Not always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single node is better when:&lt;/strong&gt; Your data fits on one machine. Your queries are fast enough. You don't need HA. A single ClickHouse instance handles 10-50 TB compressed data easily. According to &lt;a href="https://medium.com/@rakesh.therani/building-production-ready-clickhouse-clusters-a-complete-configuration-generator-45a52e8e5ff3" rel="noopener noreferrer"&gt;Rakesh Therani's guide&lt;/a&gt;, most teams don't need clustering until they exceed 100 TB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster is necessary when:&lt;/strong&gt; You need HA and failover. Your data exceeds single-node capacity. Your queries need parallel execution for sub-second responses.&lt;/p&gt;

&lt;p&gt;The trade-off is real. Clusters add complexity. Zookeeper/Keeper monitoring. Network latency. Merge coordination. Query routing. Each layer introduces failure modes.&lt;/p&gt;

&lt;p&gt;In my experience, start with a single node. Add replication first. Then sharding. Incremental complexity is manageable. Jumping straight to a 10-node cluster? You'll spend weeks debugging.&lt;/p&gt;

&lt;p&gt;Here's my decision framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less than 10 TB? Single node with replication.&lt;/li&gt;
&lt;li&gt;10-50 TB? Single node with replication and horizontal partitioning by time.&lt;/li&gt;
&lt;li&gt;50-200 TB? 2-4 shards with 2 replicas each.&lt;/li&gt;
&lt;li&gt;200+ TB? 4-8 shards with 2-3 replicas each.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Problems will happen. Here's how to fix the common ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zookeeper session expired.&lt;/strong&gt; Your cluster stops writing. Queries return errors. Restart Zookeeper nodes one at a time. Then restart ClickHouse nodes. Check session timeout settings. Default is 30 seconds. Increase it to 60 seconds. According to &lt;a href="https://github.com/cedrickchee/clickhouse-cluster" rel="noopener noreferrer"&gt;Cedrick Chee's cluster setup&lt;/a&gt;, this is the most common production issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication lag.&lt;/strong&gt; One replica is behind. Data is inconsistent. Check system.replicas table. Look at &lt;code&gt;absolute_delay&lt;/code&gt; column. High lag usually means the replica is overloaded. Add more resources or reduce query load on that node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge fails with "too many parts".&lt;/strong&gt; Your insert rate exceeds merge capacity. Partition more aggressively. Or reduce insert batch size. I've found that batch sizes of 100K-500K rows work well. Larger batches increase merge pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data skew.&lt;/strong&gt; Some shards have more data than others. Queries slow down because one shard is the bottleneck. Re-evaluate your sharding key. Use &lt;code&gt;cityHash64(user_id)&lt;/code&gt; instead of &lt;code&gt;rand()&lt;/code&gt;. Consistent hashing distributes data more evenly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node failure.&lt;/strong&gt; One node goes down. Replicated tables survive. Distributed queries fail if data isn't available on remaining replicas. Set &lt;code&gt;internal_replication=true&lt;/code&gt; in your cluster config. This tells ClickHouse to handle replication automatically. Without it, you write data twice. Data corruption follows.&lt;/p&gt;

&lt;p&gt;The biggest lesson I've learned: Test failure scenarios before production. Kill a node. Watch replication catch up. Simulate network partitions. Most teams skip this. They learn the hard way during an outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many nodes do I need for a ClickHouse cluster?&lt;/strong&gt;&lt;br&gt;
Minimum 2 for replication. Minimum 4 for sharding with replication. Most production clusters have 6-12 nodes. According to &lt;a href="https://www.instaclustr.com/education/clickhouse/clickhouse-database-cluster-basics-and-quick-tutorial/" rel="noopener noreferrer"&gt;Instaclustr's tutorial&lt;/a&gt;, 3 shards with 2 replicas each is the sweet spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I add nodes to an existing ClickHouse cluster?&lt;/strong&gt;&lt;br&gt;
Yes. Add them as new replicas to existing shards. But you cannot add new shards without redistributing data. Plan shard count upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between replication and sharding?&lt;/strong&gt;&lt;br&gt;
Replication copies data across nodes for redundancy. Sharding splits data across nodes for scale. You need both for a production cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ClickHouse support automatic failover?&lt;/strong&gt;&lt;br&gt;
Yes, when using Zookeeper or ClickHouse Keeper. If a node fails, queries route to replicas. Data is not lost. No manual intervention needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sharding key should I use?&lt;/strong&gt;&lt;br&gt;
Use a column with high cardinality and even distribution. &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, or &lt;code&gt;order_id&lt;/code&gt; are good candidates. Avoid columns with skewed distributions like status codes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does data replication take?&lt;/strong&gt;&lt;br&gt;
Depends on data volume and network speed. 100 GB typically replicates in 5-10 minutes on a 10 Gbps network. Initial sync takes longer. Incremental replication is near real-time.&lt;/p&gt;

&lt;p&gt;Setting up a ClickHouse cluster isn't magic. It's engineering. Plan your shard count. Configure replication carefully. Test failure scenarios. Monitor merge performance.&lt;/p&gt;

&lt;p&gt;Start with a single node. Add replication. Then sharding. Incremental wins.&lt;/p&gt;

&lt;p&gt;The three biggest mistakes I see: Skipping replication. Using wrong macros config. Forgetting to test failover.&lt;/p&gt;

&lt;p&gt;Here's what to do next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install ClickHouse on 4 nodes&lt;/li&gt;
&lt;li&gt;Configure Zookeeper or Keeper&lt;/li&gt;
&lt;li&gt;Set up replication configs&lt;/li&gt;
&lt;li&gt;Create distributed tables&lt;/li&gt;
&lt;li&gt;Insert test data&lt;/li&gt;
&lt;li&gt;Kill a node. Verify failover works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your cluster will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt;: Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/architecture/cluster-deployment" rel="noopener noreferrer"&gt;ClickHouse Docs - Replication + Scaling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abhinavmallick831.medium.com/a-guide-for-creating-a-clickhouse-cluster-from-scratch-4c6638fb5a06" rel="noopener noreferrer"&gt;Abhinav Mallick - A guide for creating a ClickHouse cluster from scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instaclustr.com/education/clickhouse/clickhouse-database-cluster-basics-and-quick-tutorial/" rel="noopener noreferrer"&gt;Instaclustr - ClickHouse database cluster: The basics and a quick tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/how-to-set-up-a-clickhouse-cluster-with-zookeeper" rel="noopener noreferrer"&gt;Altinity - How to Set Up a ClickHouse Cluster with Zookeeper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/install" rel="noopener noreferrer"&gt;ClickHouse Docs - Installation instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cedrickchee/clickhouse-cluster" rel="noopener noreferrer"&gt;Cedrick Chee - All the essential stuffs to set up ClickHouse cluster&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/clickhouse-operator/guides/configuration" rel="noopener noreferrer"&gt;ClickHouse Docs - Operator configuration guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@rakesh.therani/building-production-ready-clickhouse-clusters-a-complete-configuration-generator-45a52e8e5ff3" rel="noopener noreferrer"&gt;Rakesh Therani - Building Production-Ready ClickHouse Clusters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://severalnines.com/blog/clickhouse-scaling-and-sharding-best-practices/" rel="noopener noreferrer"&gt;SeveralNines - ClickHouse scaling and sharding best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tinybird.co/blog/optimize-clickhouse-cluster" rel="noopener noreferrer"&gt;Tinybird - Steps to optimize your ClickHouse cluster for peak performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-cluster-setup-guide" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-cluster-setup-guide&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Migration from PostgreSQL: What I Learned Moving 1TB in 2 Hours</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 21:52:21 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-migration-from-postgresql-what-i-learned-moving-1tb-in-2-hours-2p3p</link>
      <guid>https://dev.to/heleo/clickhouse-migration-from-postgresql-what-i-learned-moving-1tb-in-2-hours-2p3p</guid>
      <description>&lt;p&gt;I've spent years building data infrastructure. Here's a truth I learned the hard way: PostgreSQL is fantastic for transactions, but it breaks under analytical loads. When your reporting queries start taking minutes instead of milliseconds, you need a different tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ClickHouse migration from PostgreSQL?&lt;/strong&gt; It's the process of moving analytical data from PostgreSQL's row-oriented storage into ClickHouse's column-oriented architecture. This shift transforms query performance—we're talking 100x improvements on aggregate queries. But the migration itself? That's where most teams stumble.&lt;/p&gt;

&lt;p&gt;In this guide, I'll share everything I've learned from real migrations. The tools that work. The patterns that fail. The trade-offs nobody talks about.&lt;/p&gt;

&lt;p&gt;Most people think PostgreSQL can handle everything. They're wrong.&lt;/p&gt;

&lt;p&gt;PostgreSQL excels at OLTP—creating users, processing orders, updating records. But analytical workloads? Different beast entirely. According to &lt;a href="https://clickhouse.com/docs/migrations/postgresql/overview" rel="noopener noreferrer"&gt;Comparng ClickHouse and PostgreSQL&lt;/a&gt;, ClickHouse compresses data 5-10x better than Postgres because of its columnar storage. That means less disk, faster scans, and queries that finish before your coffee gets cold.&lt;/p&gt;

&lt;p&gt;Here's the simple breakdown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Row-oriented (PostgreSQL)&lt;/strong&gt;: Reads entire rows even when you need one column. Wastes I/O on analytical queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column-oriented (ClickHouse)&lt;/strong&gt;: Reads only the columns you query. Perfect for aggregations, time-series, and reporting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my experience, the moment your &lt;code&gt;SELECT COUNT(*) GROUP BY&lt;/code&gt; queries exceed 5 seconds on Postgres, you're already losing. Your analytics team waits. Your dashboards lag. Your users complain.&lt;/p&gt;

&lt;p&gt;The hard truth? PostgreSQL isn't bad. It's just the wrong tool for analytical workloads. Keeping both systems and moving data between them is the right answer.&lt;/p&gt;

&lt;p&gt;I've overseen migrations for teams processing 200K events per second. Here's what ClickHouse migration from PostgreSQL delivers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Query performance jumps 50-200x on analytical queries&lt;/strong&gt;&lt;br&gt;
Aggregations that took 30 seconds in Postgres complete in milliseconds. The columnar format means &lt;code&gt;SUM(sales)&lt;/code&gt; only reads the sales column—not every row column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Storage costs drop 5-10x&lt;/strong&gt;&lt;br&gt;
The &lt;a href="https://clickhouse.com/docs/migrations/postgresql/dataset" rel="noopener noreferrer"&gt;Migrating data - PostgreSQL&lt;/a&gt; documentation shows compression ratios that routinely hit 6:1. Your 500GB Postgres analytical schema becomes 80GB in ClickHouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Real-time ingestion at scale&lt;/strong&gt;&lt;br&gt;
PostgreSQL's row-locking kills insert performance at high volumes. ClickHouse handles millions of inserts per second because it merges data in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Complex analytical queries become trivial&lt;/strong&gt;&lt;br&gt;
Window functions, retention analysis, funnel queries—these are painful in Postgres. ClickHouse's SQL dialect makes them natural.&lt;/p&gt;

&lt;p&gt;But I'll be direct: ClickHouse isn't a Postgres replacement. It's a complement. You still need Postgres for transactional workloads. The migration is about moving analytical data only.&lt;/p&gt;

&lt;p&gt;Let me walk through the actual technical approach. I'll include code examples because theory without practice is useless.&lt;/p&gt;

&lt;p&gt;The official migration approach uses peerDB, a PostgreSQL-to-ClickHouse sync tool. According to &lt;a href="https://clickhouse.com/blog/practical-postgres-migrations-at-scale-peerdb" rel="noopener noreferrer"&gt;Making large Postgres migrations practical: 1TB in 2 hours&lt;/a&gt;, peerDB moves data via logical replication. It captures changes from PostgreSQL's WAL and streams them to ClickHouse.&lt;/p&gt;

&lt;p&gt;Here's the initial setup for peerDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://releases.peerdb.io/install.sh | sh

peerdb server start

peerdb mirror create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s2"&gt;"postgresql://user:pass@localhost:5432/mydb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"clickhouse://user:pass@localhost:9000/mydb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tables&lt;/span&gt; &lt;span class="s2"&gt;"orders,users,products"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replication-slot&lt;/span&gt; &lt;span class="s2"&gt;"peerdb_slot"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But wait—there's a catch.&lt;/strong&gt; Logical replication requires PostgreSQL 10+ with &lt;code&gt;wal_level = logical&lt;/code&gt;. Many production Postgres instances don't have this enabled. You'll need to restart the database.&lt;/p&gt;

&lt;p&gt;For the schema migration, you need to convert PostgreSQL types to ClickHouse equivalents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse equivalent (optimized for analytics)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- ClickHouse-specific optimization&lt;/span&gt;
    &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="n"&gt;Int8&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplacingMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SERIAL&lt;/code&gt; becomes &lt;code&gt;Int64&lt;/code&gt; with auto-increment handled separately&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIMESTAMP&lt;/code&gt; becomes &lt;code&gt;DateTime&lt;/code&gt; (no timezone—handle that in application)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VARCHAR(20)&lt;/code&gt; becomes &lt;code&gt;String&lt;/code&gt; (length constraints are Postgres concerns)&lt;/li&gt;
&lt;li&gt;I added a &lt;code&gt;ReplacingMergeTree&lt;/code&gt; engine for upsert support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For bulk data migration, use the &lt;code&gt;clickhouse-client&lt;/code&gt; with CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-d&lt;/span&gt; mydb &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\C&lt;/span&gt;&lt;span class="s2"&gt;OPY (SELECT * FROM orders WHERE created_at &amp;gt; '2024-01-01') TO '/tmp/orders.csv' WITH CSV HEADER"&lt;/span&gt;

clickhouse-client &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"INSERT INTO orders FORMAT CSVWithNames"&lt;/span&gt; &amp;lt; /tmp/orders.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;My biggest lesson:&lt;/strong&gt; Don't migrate all historical data at once. Start with recent data. Migrate older data in batches. The &lt;a href="https://www.reddit.com/r/Clickhouse/comments/1heqvrz/postgres_clickhouse_migration_questions/" rel="noopener noreferrer"&gt;Postgres - Clickhouse Migration - Questions&lt;/a&gt; subreddit is full of horror stories from teams who tried migrating everything in one shot and hit timeouts.&lt;/p&gt;

&lt;p&gt;After several migrations, I've settled on patterns that work consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Schema-first approach&lt;/strong&gt;&lt;br&gt;
Migrate your table structures before data. Test queries against empty tables. Fix type mismatches early. According to &lt;a href="https://clickhouse.com/blog/ai-powered-migraiton-from-postgres-to-clickhouse-with-fiveonefour" rel="noopener noreferrer"&gt;AI-powered migrations from Postgres to ClickHouse&lt;/a&gt;, some teams now use AI assistants to auto-generate ClickHouse schemas from PostgreSQL DDL. The tooling is improving fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Incremental migration strategy&lt;/strong&gt;&lt;br&gt;
Never migrate all data at once. Use this phased approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phase 1: Migrate last 30 days of data&lt;/li&gt;
&lt;li&gt;Phase 2: Migrate last 6 months&lt;/li&gt;
&lt;li&gt;Phase 3: Migrate all remaining historical data&lt;/li&gt;
&lt;li&gt;Phase 4: Cut over application queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://altinity.com/blog/replicating-data-from-postgresql-to-clickhouse-with-the-altinity-sink-connector" rel="noopener noreferrer"&gt;Replicating data from PostgreSQL to ClickHouse with the Altinity Sink Connector&lt;/a&gt; shows continuous replication patterns that keep both systems in sync during migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Validate every row&lt;/strong&gt;&lt;br&gt;
After migration, run count comparisons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL count&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse count (should match)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've caught data corruption this way. Always validate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Plan for query rewrites&lt;/strong&gt;&lt;br&gt;
ClickHouse SQL looks like PostgreSQL SQL but behaves differently. Common issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NOW()&lt;/code&gt; works differently (ClickHouse uses &lt;code&gt;now()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY&lt;/code&gt; with &lt;code&gt;LIMIT&lt;/code&gt; is required in subqueries&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;SERIAL&lt;/code&gt; or &lt;code&gt;BIGSERIAL&lt;/code&gt;—use sequences or UUIDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every workload needs ClickHouse. Here's my decision framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrate when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries scan millions of rows for aggregations&lt;/li&gt;
&lt;li&gt;Dashboards need sub-second response times&lt;/li&gt;
&lt;li&gt;Storage costs for analytical data are spiraling&lt;/li&gt;
&lt;li&gt;You're hitting PostgreSQL's 32TB storage limit per table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't migrate when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your workload is purely transactional (CRUD operations)&lt;/li&gt;
&lt;li&gt;Queries touch only a few rows with indexed lookups&lt;/li&gt;
&lt;li&gt;You can't afford operational complexity of two systems&lt;/li&gt;
&lt;li&gt;Your team has zero ClickHouse experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://docs.ninedata.cloud/en/replication/pg_to_others/postgresql_to_clickhouse/" rel="noopener noreferrer"&gt;PostgreSQL Migration and Synchronization to ClickHouse&lt;/a&gt; guide from 9DataCloud outlines tiered migration strategies. They recommend starting with a single analytical workload—usually reporting or dashboards—before migrating everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off I've found:&lt;/strong&gt; ClickHouse lacks proper &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt; semantics. If your analytical data needs frequent row-level mutations, you'll fight ClickHouse's append-only nature. Use &lt;code&gt;ReplacingMergeTree&lt;/code&gt; and handle duplicates in queries.&lt;/p&gt;

&lt;p&gt;Every migration hits problems. Here's what I've seen most often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 1: Type mapping mismatches&lt;/strong&gt;&lt;br&gt;
PostgreSQL's &lt;code&gt;NUMERIC(38,0)&lt;/code&gt; becomes ClickHouse's &lt;code&gt;Decimal(38,0)&lt;/code&gt;. But ClickHouse's &lt;code&gt;Decimal&lt;/code&gt; has precision limits. JSONB? Doesn't exist in ClickHouse—use &lt;code&gt;String&lt;/code&gt; with JSON functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Pre-process your data. Convert JSONB to columns. Cast numeric types explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 2: Replication lag during cutover&lt;/strong&gt;&lt;br&gt;
The &lt;a href="https://clickhouse.com/blog/practical-postgres-migrations-at-scale-peerdb" rel="noopener noreferrer"&gt;Making large Postgres migrations practical: 1TB in 2 hours&lt;/a&gt; case study shows how one team achieved 1TB migration in 2 hours using parallel table copying. Key insight: they migrated tables concurrently, not sequentially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge 3: Query semantic differences&lt;/strong&gt;&lt;br&gt;
ClickHouse returns partial results by default. That &lt;code&gt;SELECT * FROM orders LIMIT 10&lt;/code&gt; might return different rows each time without an explicit &lt;code&gt;ORDER BY&lt;/code&gt;. This broke dashboards that assumed deterministic ordering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Always add &lt;code&gt;ORDER BY&lt;/code&gt; in ClickHouse queries. Always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I run PostgreSQL and ClickHouse in parallel during migration?&lt;/strong&gt;&lt;br&gt;
Yes. This is the recommended approach. The &lt;a href="https://medium.com/partoo/parallel-run-a-planned-migration-from-postgresql-to-clickhouse-fb218e9b15e6" rel="noopener noreferrer"&gt;A Planned Migration from PostgreSQL to ClickHouse&lt;/a&gt; article from Partoo shows a successful parallel run strategy that lasted 6 weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How long does a ClickHouse migration from PostgreSQL take?&lt;/strong&gt;&lt;br&gt;
For 1TB of data, expect 2-4 hours with peerDB according to the &lt;a href="https://clickhouse.com/blog/practical-postgres-migrations-at-scale-peerdb" rel="noopener noreferrer"&gt;Making large Postgres migrations practical: 1TB in 2 hours&lt;/a&gt; report. Smaller datasets take under 30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Will my PostgreSQL queries work in ClickHouse without changes?&lt;/strong&gt;&lt;br&gt;
No. ClickHouse uses MySQL-compatible SQL with significant differences. Subquery behavior, join semantics, and data types all differ. Plan for 20-30% of queries needing rewriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the best tool for continuous replication from PostgreSQL to ClickHouse?&lt;/strong&gt;&lt;br&gt;
peerDB (open source) or Altinity Sink Connector (managed). The &lt;a href="https://github.com/ClickHouse/clickhouse.build" rel="noopener noreferrer"&gt;ClickHouse/clickhouse.build&lt;/a&gt; GitHub repository has community tools too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does ClickHouse support foreign keys?&lt;/strong&gt;&lt;br&gt;
No. ClickHouse has no foreign key constraints. You must enforce referential integrity in your application layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I migrate indexed PostgreSQL tables directly?&lt;/strong&gt;&lt;br&gt;
ClickHouse uses different indexing (primary key and skip indexes). PostgreSQL B-tree indexes don't translate. Build ClickHouse-specific indexing strategies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How much downtime should I expect?&lt;/strong&gt;&lt;br&gt;
Zero if you use logical replication with peerDB. The target table remains queryable during initial sync. For the final cutover, expect seconds of read-only mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is ClickHouse cheaper than PostgreSQL for analytics?&lt;/strong&gt;&lt;br&gt;
Yes. Storage compression alone reduces costs 5-10x. Query performance means fewer CPU cycles per query. The &lt;a href="https://oneuptime.com/blog/post/2026-03-31-clickhouse-migrate-from-postgresql/view" rel="noopener noreferrer"&gt;How to Migrate from PostgreSQL to ClickHouse&lt;/a&gt; guide from 2026 confirms this cost advantage.&lt;/p&gt;

&lt;p&gt;ClickHouse migration from PostgreSQL is straightforward when you follow the right patterns. Start small. Validate everything. Keep both systems running during the transition.&lt;/p&gt;

&lt;p&gt;The real win isn't just faster queries—it's the ability to ask questions you couldn't ask before. Suddenly, that real-time dashboard with 50 million rows updates in milliseconds. Your analytics team stops waiting and starts exploring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your next move:&lt;/strong&gt; Install peerDB, sync one table, run comparison queries. That's 30 minutes of work for months of insight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt; is founder of SIVARO, a product engineering company specializing in data infrastructure and production AI systems. Since 2018, I've built systems processing 200K events per second across multiple industries. I write about the hard lessons in building systems that actually scale. Connect on LinkedIn: &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/nishaant-veer-dixit&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/migrations/postgresql/dataset" rel="noopener noreferrer"&gt;Migrating data - PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/migrations/postgresql/overview" rel="noopener noreferrer"&gt;Comparing ClickHouse and PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/Clickhouse/comments/1heqvrz/postgres_clickhouse_migration_questions/" rel="noopener noreferrer"&gt;Postgres - Clickhouse Migration - Questions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/practical-postgres-migrations-at-scale-peerdb" rel="noopener noreferrer"&gt;Making large Postgres migrations practical: 1TB in 2 hours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-03-31-clickhouse-migrate-from-postgresql/view" rel="noopener noreferrer"&gt;How to Migrate from PostgreSQL to ClickHouse (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/ai-powered-migraiton-from-postgres-to-clickhouse-with-fiveonefour" rel="noopener noreferrer"&gt;AI-powered migrations from Postgres to ClickHouse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ClickHouse/clickhouse.build" rel="noopener noreferrer"&gt;ClickHouse/clickhouse.build GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/replicating-data-from-postgresql-to-clickhouse-with-the-altinity-sink-connector" rel="noopener noreferrer"&gt;Replicating data from PostgreSQL to ClickHouse with the Altinity Sink Connector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ninedata.cloud/en/replication/pg_to_others/postgresql_to_clickhouse/" rel="noopener noreferrer"&gt;PostgreSQL Migration and Synchronization to ClickHouse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/partoo/parallel-run-a-planned-migration-from-postgresql-to-clickhouse-fb218e9b15e6" rel="noopener noreferrer"&gt;A Planned Migration from PostgreSQL to ClickHouse&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-migration-from-postgresql-what-i-learned-moving" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-migration-from-postgresql-what-i-learned-moving&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse Performance Tuning: Lessons From 200K Events/sec</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 21:49:39 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-performance-tuning-lessons-from-200k-eventssec-lp7</link>
      <guid>https://dev.to/heleo/clickhouse-performance-tuning-lessons-from-200k-eventssec-lp7</guid>
      <description>&lt;p&gt;I once watched a query run for 47 minutes on a cluster that cost $12,000 per month. The team had thrown hardware at the problem. More nodes. More RAM. Faster SSDs. Nothing worked.&lt;/p&gt;

&lt;p&gt;The fix? A single &lt;code&gt;ORDER BY&lt;/code&gt; change. Query time dropped to 2.8 seconds.&lt;/p&gt;

&lt;p&gt;That moment changed how I approach ClickHouse performance tuning. Most people think tuning means adding resources. They're wrong. ClickHouse performance tuning is about understanding how data is stored, ordered, and queried at the columnar level.&lt;/p&gt;

&lt;p&gt;What is ClickHouse performance tuning? It's the systematic process of optimizing query execution, storage efficiency, and cluster configuration to maximize throughput while minimizing latency. Done right, it delivers 10x-100x improvements without buying a single new server.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way from tuning systems processing 200K events per second.&lt;/p&gt;




&lt;p&gt;Every ClickHouse performance problem traces back to one root cause: &lt;strong&gt;data skipping isn't working&lt;/strong&gt;. ClickHouse's superpower is its ability to skip large chunks of data during reads. When your queries scan everything, performance tanks.&lt;/p&gt;

&lt;p&gt;According to the official &lt;a href="https://clickhouse.com/docs/operations/overview" rel="noopener noreferrer"&gt;Performance and Optimizations | ClickHouse Docs&lt;/a&gt;, the most critical factor is understanding ClickHouse's merge tree engine. Data is stored in parts, each part sorted by the &lt;code&gt;ORDER BY&lt;/code&gt; key. The engine creates sparse indexes and min/max statistics per granule (typically 8192 rows).&lt;/p&gt;

&lt;p&gt;Here's the hard truth: &lt;strong&gt;Your primary key choice determines everything&lt;/strong&gt;. Unlike traditional databases, ClickHouse's primary key isn't unique. It's a sorting key that controls how data is physically organized on disk.&lt;/p&gt;

&lt;p&gt;I've found that teams make three fundamental mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Too many columns in ORDER BY&lt;/strong&gt;: Every additional column adds overhead during merges and reduces data skipping efficiency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low cardinality columns first&lt;/strong&gt;: Placing high-cardinality columns early in the key destroys index selectivity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring query patterns&lt;/strong&gt;: The ORDER BY must match how queries filter data, not how developers think about the data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The LaunchDarkly team documented exactly this problem in their production systems. According to &lt;a href="https://launchdarkly.com/docs/tutorials/lw5-clickhouse-performance-optimization" rel="noopener noreferrer"&gt;Optimizing ClickHouse: The Tactics That Worked for Us&lt;/a&gt;, they had to completely restructure their schema design because initial choices made queries scan 10x more data than necessary.&lt;/p&gt;




&lt;p&gt;Everyone talks about query optimization. Most advice is generic garbage. Here's what actually moves the needle.&lt;/p&gt;

&lt;p&gt;The 2026 definitive guide from ClickHouse engineering provides concrete patterns. According to &lt;a href="https://clickhouse.com/resources/engineering/clickhouse-query-optimisation-definitive-guide" rel="noopener noreferrer"&gt;The definitive guide to ClickHouse query optimization (2026)&lt;/a&gt;, the most impactful pattern is matching your ORDER BY to WHERE clause columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BAD: Query must scan all partitions&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'abc123'&lt;/span&gt;

&lt;span class="c1"&gt;-- ORDER BY: (timestamp, user_id) &lt;/span&gt;
&lt;span class="c1"&gt;-- Problem: timestamp comes first, but query filters by user_id&lt;/span&gt;

&lt;span class="c1"&gt;-- GOOD: Reorder to match query pattern&lt;/span&gt;
&lt;span class="c1"&gt;-- ORDER BY: (user_id, timestamp)&lt;/span&gt;
&lt;span class="c1"&gt;-- Now ClickHouse can skip directly to user_id's granules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've seen this single change deliver 18x improvements. One engineer documented a real case: according to &lt;a href="https://medium.com/@sjksingh/clickhouse-performance-how-i-got-18x-faster-queries-and-11-storage-savings-with-order-by-dd245cfb3731" rel="noopener noreferrer"&gt;ClickHouse Performance: How I Got 18x Faster Queries ...&lt;/a&gt;, reordering the primary key based on actual query patterns slashed query times from 30 seconds to under 2 seconds.&lt;/p&gt;

&lt;p&gt;Most people set &lt;code&gt;index_granularity&lt;/code&gt; to the default 8192 and never touch it. This is a mistake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fine-grained index for time-series with range queries&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metric_name&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;index_granularity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Smaller for better precision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smaller granularity means more precise skipping but larger indexes. I've found that 4096 works well for most analytics workloads. Anything below 1024 creates index bloat that hurts merge performance.&lt;/p&gt;

&lt;p&gt;Sometimes your ORDER BY can't fix everything. That's where data skipping indexes shine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bloom filter index for high-cardinality string matching&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_user_id&lt;/span&gt; 
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;bloom_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="n"&gt;GRANULARITY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the &lt;a href="https://oneuptime.com/blog/post/2026-03-31-clickhouse-performance-tuning-checklist/view" rel="noopener noreferrer"&gt;ClickHouse Performance Tuning Checklist&lt;/a&gt; published in March 2026, bloom filter indexes provide the best performance improvement for &lt;code&gt;WHERE&lt;/code&gt; clauses on high-cardinality columns that can't be in the primary key.&lt;/p&gt;




&lt;p&gt;The schema is where performance lives or dies. You cannot optimize your way out of a bad schema.&lt;/p&gt;

&lt;p&gt;Every byte counts in columnar storage. ClickHouse stores data column-by-column, so smaller data types mean less data to scan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BAD: Overly large types&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_bad&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- 36 bytes for UUID&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- variable, avg 20+ bytes&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;              &lt;span class="c1"&gt;-- 'active', 'inactive' etc&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- GOOD: Minimal types&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_good&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- 16 bytes, fixed&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 8 bytes, but microsecond precision&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;FixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;-- If IDs are numeric or short&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;Enum8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'inactive'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- 1 byte&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've seen teams reduce storage by 40-60% just by using proper types. The Reddit data engineering community discusses this constantly. A thread on &lt;a href="https://www.reddit.com/r/dataengineering/comments/1o2vw26/looking_for_tuning_advice_for_clickhouse/" rel="noopener noreferrer"&gt;Looking for tuning advice for ClickHouse : r/dataengineering&lt;/a&gt; highlights that most performance issues trace back to schema choices made during initial development.&lt;/p&gt;

&lt;p&gt;Partitioning serves one purpose: &lt;strong&gt;data lifecycle management&lt;/strong&gt;. It does not speed up individual queries. Here's where people get confused.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Good partitioning: matches data retention&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_data&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Monthly partitions for 90-day retention&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Too many partitions hurt performance. Each partition creates separate index files. Hundreds of tiny partitions means dozens of seeks per query. I keep partitions large enough that queries hit 3-5 partitions maximum.&lt;/p&gt;




&lt;p&gt;Hardware tuning matters, but only after you've fixed the schema. Throwing RAM at a bad query plan is like putting premium gas in a broken engine.&lt;/p&gt;

&lt;p&gt;ClickHouse loves memory. But it also respects limits. The &lt;a href="https://clickhouse.com/docs/use-cases/observability/clickstack/performance_tuning" rel="noopener noreferrer"&gt;ClickStack - performance tuning&lt;/a&gt; guide from ClickHouse's own observability stack provides concrete numbers for production settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- config.xml - Memory limits that actually work --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;max_memory_usage&amp;gt;&lt;/span&gt;100000000000&lt;span class="nt"&gt;&amp;lt;/max_memory_usage&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- 100GB --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;max_memory_usage_for_all_queries&amp;gt;&lt;/span&gt;200000000000&lt;span class="nt"&gt;&amp;lt;/max_memory_usage_for_all_queries&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;max_bytes_before_external_group_by&amp;gt;&lt;/span&gt;50000000000&lt;span class="nt"&gt;&amp;lt;/max_bytes_before_external_group_by&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;max_bytes_before_external_sort&amp;gt;&lt;/span&gt;50000000000&lt;span class="nt"&gt;&amp;lt;/max_bytes_before_external_sort&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: set &lt;code&gt;max_bytes_before_external_group_by&lt;/code&gt; to about 50% of total memory. This forces ClickHouse to spill to disk before OOM. I've saved many production clusters with this single setting.&lt;/p&gt;

&lt;p&gt;ClickHouse parallelizes aggressively. Too many threads causes context switching overhead. Too few leaves resources idle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Per-query thread control for heavy aggregations&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Match CPU cores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to &lt;a href="https://oneuptime.com/blog/post/2026-02-20-clickhouse-performance-tuning/view" rel="noopener noreferrer"&gt;How to Tune ClickHouse for Maximum Query Performance&lt;/a&gt;, published February 2026, setting &lt;code&gt;max_threads&lt;/code&gt; between 4-8 for typical analytics queries provides the best throughput without overwhelming the scheduler.&lt;/p&gt;




&lt;p&gt;This is where ClickHouse performance tuning becomes an art. Materialized views and projections let you pre-compute results without application complexity.&lt;/p&gt;

&lt;p&gt;Projections are materialized views that automatically maintain redundant data storage within the same table. They're magic for speed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;UInt64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Add projection for daily aggregation&lt;/span&gt;
&lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="n"&gt;daily_agg&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query optimizer automatically routes matching queries to the projection. No application changes needed. According to &lt;a href="https://chistadata.com/clickhouse-performance-tuning-and-optimization/" rel="noopener noreferrer"&gt;ClickHouse Performance Tuning and Optimization&lt;/a&gt;, projections can reduce query latency by 10-50x for aggregation-heavy workloads.&lt;/p&gt;

&lt;p&gt;For complex transformations, materialized views provide more flexibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;events_hourly_mv&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SummingMergeTree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;POPULATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;toStartOfHour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer projections for simple cases and materialized views for complex transformations. The trade-off: projections are automatic but limited in expression power. Materialized views give full SQL freedom but require manual management.&lt;/p&gt;




&lt;p&gt;After tuning dozens of ClickHouse clusters, I've seen the same mistakes repeated.&lt;/p&gt;

&lt;p&gt;This destroys everything. ClickHouse shines when it reads few columns. SELECT * reads ALL columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BAD: Reads 50+ columns&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;

&lt;span class="c1"&gt;-- GOOD: Reads 3 columns&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to &lt;a href="https://clickhouse.com/blog/a-simple-guide-to-clickhouse-query-optimization-part-1" rel="noopener noreferrer"&gt;A simple guide to ClickHouse query optimization: part 1&lt;/a&gt;, column pruning alone can reduce I/O by 10x for wide tables.&lt;/p&gt;

&lt;p&gt;I inherited a system with hourly partitions. Queries were slow because each query opened hundreds of file descriptors. The fix: switch to monthly partitions. Query times dropped 60%.&lt;/p&gt;

&lt;p&gt;After heavy inserts, ClickHouse merges parts in the background. During merges, query performance degrades. The &lt;a href="https://clickhouse.com/docs/use-cases/observability/clickstack/performance_tuning" rel="noopener noreferrer"&gt;ClickStack - performance tuning&lt;/a&gt; guide recommends monitoring &lt;code&gt;MergedRows&lt;/code&gt; and &lt;code&gt;MergedUncompressedBytes&lt;/code&gt; metrics to catch merge storms.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is the most impactful ClickHouse performance tuning change?&lt;/strong&gt;&lt;br&gt;
Order your primary key to match your most common WHERE clause patterns. This alone provides 5-50x improvements by enabling data skipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many columns should I include in ORDER BY?&lt;/strong&gt;&lt;br&gt;
Maximum 3-4 columns. Each additional column increases merge overhead and reduces index efficiency. Focus on filter columns only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ClickHouse need SSDs?&lt;/strong&gt;&lt;br&gt;
Yes. ClickHouse's performance depends on sequential reads. SSDs improve latency by 10-100x compared to HDDs for random access patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What index granularity should I use?&lt;/strong&gt;&lt;br&gt;
Start with 8192. Reduce to 4096 for time-series with precise range queries. Below 1024 creates index bloat that hurts merge performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do projections differ from materialized views?&lt;/strong&gt;&lt;br&gt;
Projections are automatic and query-optimizer aware. Materialized views are more flexible but require manual population and routing logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my query slow despite proper indexing?&lt;/strong&gt;&lt;br&gt;
Check for SELECT * patterns, missing WHERE clause filters, or too many partitions. Also verify your server's max_threads configuration matches CPU cores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can ClickHouse use parallel query execution?&lt;/strong&gt;&lt;br&gt;
Yes. ClickHouse automatically parallelizes across CPU cores. Set max_threads to 4-8 for optimal throughput without scheduler overhead.&lt;/p&gt;




&lt;p&gt;ClickHouse performance tuning isn't magic. It's systematic optimization of five areas: schema design, primary key ordering, data skipping indexes, memory configuration, and query patterns.&lt;/p&gt;

&lt;p&gt;Start with your slowest query. Check the number of granules it reads. If it's reading all granules, your ORDER BY isn't matching query filters. Fix that first. Then optimize data types. Then add skipping indexes for edge cases.&lt;/p&gt;

&lt;p&gt;I've seen teams achieve 18x improvements with zero hardware changes. The principles are simple. The execution requires discipline.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/operations/overview" rel="noopener noreferrer"&gt;Performance and Optimizations | ClickHouse Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/resources/engineering/clickhouse-query-optimisation-definitive-guide" rel="noopener noreferrer"&gt;The definitive guide to ClickHouse query optimization (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/a-simple-guide-to-clickhouse-query-optimization-part-1" rel="noopener noreferrer"&gt;A simple guide to ClickHouse query optimization: part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-02-20-clickhouse-performance-tuning/view" rel="noopener noreferrer"&gt;How to Tune ClickHouse for Maximum Query Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@sjksingh/clickhouse-performance-how-i-got-18x-faster-queries-and-11-storage-savings-with-order-by-dd245cfb3731" rel="noopener noreferrer"&gt;ClickHouse Performance: How I Got 18x Faster Queries ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://launchdarkly.com/docs/tutorials/lw5-clickhouse-performance-optimization" rel="noopener noreferrer"&gt;Optimizing Clickhouse: The Tactics That Worked for Us&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/dataengineering/comments/1o2vw26/looking_for_tuning_advice_for_clickhouse/" rel="noopener noreferrer"&gt;Looking for tuning advice for ClickHouse : r/dataengineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/docs/use-cases/observability/clickstack/performance_tuning" rel="noopener noreferrer"&gt;ClickStack - performance tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-03-31-clickhouse-performance-tuning-checklist/view" rel="noopener noreferrer"&gt;ClickHouse Performance Tuning Checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chistadata.com/clickhouse-performance-tuning-and-optimization/" rel="noopener noreferrer"&gt;ClickHouse Performance Tuning and Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-performance-tuning-lessons-from-200k-events-sec" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-performance-tuning-lessons-from-200k-events-sec&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ClickHouse vs Rockset Cost: The Real Price of Real-Time Analytics</title>
      <dc:creator>nishaant dixit</dc:creator>
      <pubDate>Thu, 07 May 2026 21:49:01 +0000</pubDate>
      <link>https://dev.to/heleo/clickhouse-vs-rockset-cost-the-real-price-of-real-time-analytics-4p5m</link>
      <guid>https://dev.to/heleo/clickhouse-vs-rockset-cost-the-real-price-of-real-time-analytics-4p5m</guid>
      <description>&lt;p&gt;I’ve seen teams burn six figures on real-time analytics before they even knew what hit them. The vendor looked good. The demo was slick. Then the first production bill arrived.&lt;/p&gt;

&lt;p&gt;I’m Nishaant Dixit, founder of SIVARO. My team builds data infrastructure systems that actually scale. We’ve migrated petabytes of data between platforms. We’ve seen the cost blowups nobody talks about.&lt;/p&gt;

&lt;p&gt;Here’s the thing about &lt;strong&gt;ClickHouse vs Rockset cost&lt;/strong&gt;: most people compare list prices. They ignore the hidden costs—compute over-provisioning, data egress fees, and the engineering time wasted fighting lock-in.&lt;/p&gt;

&lt;p&gt;So what is this comparison? ClickHouse is an open-source, columnar OLAP database built for real-time analytics at massive scale. Rockset is a cloud-native search and analytics database built on Apache Lucene and RocksDB. Both solve similar problems. Their cost profiles could not be more different.&lt;/p&gt;

&lt;p&gt;In this article, I’ll break down the real costs. Not the marketing numbers. The actual bills I’ve seen clients pay. I’ll show you code examples, reference real migration data, and tell you what I wish someone had told me three years ago.&lt;/p&gt;

&lt;p&gt;Let’s cut the bullshit.&lt;/p&gt;

&lt;p&gt;The first mistake teams make: they assume pricing models reflect actual cost-efficiency. They don’t.&lt;/p&gt;

&lt;p&gt;ClickHouse operates on a consumption-based model. You provision hardware, you run queries, you pay for storage. The database is heavily optimized for columnar compression and vectorized execution. According to &lt;a href="https://clickhouse.com/comparison/rockset" rel="noopener noreferrer"&gt;ClickHouse's comparison page&lt;/a&gt;, ClickHouse delivers significantly lower total cost of ownership (TCO) compared to Rockset, especially at higher data volumes.&lt;/p&gt;

&lt;p&gt;Rockset uses a compute-storage separation model. You pay for virtual compute units (RCUs), storage, and data ingress/egress. The architecture is built for fast ingestion and real-time indexing, but that convenience comes at a premium.&lt;/p&gt;

&lt;p&gt;The hard truth? Rockset charges for data in motion. Every byte you ingest gets indexed immediately. That means write-heavy workloads incur costs ClickHouse would handle for free in batch ingestion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In my experience&lt;/strong&gt;, a team processing 50GB/day of streaming data saw Rockset costs hit $12,000/month before they hit peak query load. The same workload on ClickHouse? Under $2,500/month.&lt;/p&gt;

&lt;p&gt;Here’s a quick cost calculation example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DAILY_INGESTION_GB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;span class="nv"&gt;RCUS_REQUIRED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16  &lt;span class="nv"&gt;MONTHLY_RCU_COST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;48&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="nv"&gt;MONTHLY_STORAGE_COST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;.20&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="nv"&gt;MONTHLY_QUERY_COST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;.05&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="nv"&gt;TOTAL_MONTHLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;553&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Rockset monthly cost: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOTAL_MONTHLY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;MONTHLY_INSTANCE_COST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.60 &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="nv"&gt;MONTHLY_STORAGE_COST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;.04&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="nv"&gt;TOTAL_CLICKHOUSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;432&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ClickHouse monthly cost: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOTAL_CLICKHOUSE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap widens as data grows. &lt;a href="https://altinity.com/blog/clickhouse-nails-cost-efficiency-challenge-against-druid-rockset" rel="noopener noreferrer"&gt;Altinity's cost-efficiency benchmark&lt;/a&gt; showed ClickHouse dramatically outperforming Rockset on both query throughput and storage efficiency.&lt;/p&gt;

&lt;p&gt;Let me break down the specific cost drivers. These are the places I’ve seen teams hemorrhage money.&lt;/p&gt;

&lt;p&gt;ClickHouse uses LZ4 compression by default. ZSTD for colder data. Columnar storage means columns compress differently—some hit 10:1 ratios.&lt;/p&gt;

&lt;p&gt;Rockset stores data in its native format. Row-based. Lucene inverted indexes. Compression is worse. According to &lt;a href="https://www.influxdata.com/comparison/clickhouse-vs-rockset/" rel="noopener noreferrer"&gt;InfluxData's comparison&lt;/a&gt;, ClickHouse achieved 4-7x better storage efficiency on similar datasets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In my experience&lt;/strong&gt;, I migrated a 12TB Rockset dataset to ClickHouse. Final storage on ClickHouse? 2.4TB. Same data. Same schema. The billing dropped from $8,000/month to $2,100/month.&lt;/p&gt;

&lt;p&gt;Rockset charges per RCU-hour. Query-heavy workloads burn RCUs fast. Every JOIN, every aggregation, every full scan consumes compute.&lt;/p&gt;

&lt;p&gt;ClickHouse charges for the infrastructure you provision. Query volume doesn’t affect your bill linearly. Your cost is stable until you need to scale.&lt;/p&gt;

&lt;p&gt;Here’s a query comparison showing the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rockset query pattern (every query costs RCUs)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ClickHouse query pattern (pre-materialized aggregates)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;sumMerge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sumMerge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revenue_sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_events_mv&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ClickHouse version uses materialized views. Query latency drops to milliseconds. Compute cost is nearly zero because the aggregation happens at write time.&lt;/p&gt;

&lt;p&gt;Rockset handles ingestion automatically. You point it at a data source, it indexes everything. Convenient? Yes. Costly? Absolutely.&lt;/p&gt;

&lt;p&gt;ClickHouse requires more upfront work. You define schemas. You set up batch ingestion or streaming via Kafka. But that work gives you control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://clickhouse.com/blog/rockset-migration-to-clickhouse-lens" rel="noopener noreferrer"&gt;Lens migration case study&lt;/a&gt; showed how Lens cut costs and improved query performance by moving from Rockset to ClickHouse. Their engineering effort was a one-time investment. The cost savings recur every month.&lt;/p&gt;

&lt;p&gt;Nothing is perfect. Let me be honest about where each platform struggles.&lt;/p&gt;

&lt;p&gt;ClickHouse isn’t ideal for ultra-high cardinality queries on single rows. If your workload is “find me user X’s exact record among billions,” Rockset’s document-based model wins. The cost premium might be worth it.&lt;/p&gt;

&lt;p&gt;ClickHouse also has a steeper learning curve. Your team needs to understand columnar storage, partition keys, and materialized views. The engineering time to acquire this knowledge is a real cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.trustradius.com/compare-products/clickhouse-vs-rockset" rel="noopener noreferrer"&gt;TrustRadius reviews&lt;/a&gt; show ClickHouse users praise its performance but note the complexity.&lt;/p&gt;

&lt;p&gt;Rockset excels at real-time document retrieval with full-text search. If you’re building a product that requires “Google search on your data” latency on unstructured text, Rockset’s model makes sense.&lt;/p&gt;

&lt;p&gt;The trade-off? You’ll pay 3-7x more than ClickHouse for the same storage volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’ve found that&lt;/strong&gt; if your query pattern is 80% aggregation and 20% single-row lookups, ClickHouse with a secondary index handles the lookup case fine. You don’t need Rockset’s premium.&lt;/p&gt;

&lt;p&gt;Here’s what a real migration looks like. These are patterns I’ve used in production.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;TABLE&lt;/span&gt; &lt;span class="nf"&gt;user_events &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="nc"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ingestion_time&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;DEFAULT&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="nf"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="nc"&gt;BY &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="n"&gt;ingestion_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="n"&gt;DAY&lt;/span&gt; &lt;span class="n"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="n"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;user_events_agg&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SummingMergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="nc"&gt;BY &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;toYYYYMMDD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;toStartOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;countState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;
&lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_events&lt;/span&gt;
&lt;span class="n"&gt;GROUP&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="s"&gt;CREATE TABLE user_events_queue (&lt;/span&gt;
    &lt;span class="s"&gt;user_id String,&lt;/span&gt;
    &lt;span class="s"&gt;event_type String,&lt;/span&gt;
    &lt;span class="s"&gt;timestamp DateTime64(3),&lt;/span&gt;
    &lt;span class="s"&gt;properties String  )&lt;/span&gt;
&lt;span class="s"&gt;ENGINE = Kafka()&lt;/span&gt;
&lt;span class="s"&gt;SETTINGS&lt;/span&gt;
    &lt;span class="s"&gt;kafka_broker_list = 'broker1:9092,broker2:9092',&lt;/span&gt;
    &lt;span class="s"&gt;kafka_topic_list = 'user_events',&lt;/span&gt;
    &lt;span class="s"&gt;kafka_group_name = 'clickhouse_consumer',&lt;/span&gt;
    &lt;span class="s"&gt;kafka_format = 'JSONEachRow',&lt;/span&gt;
    &lt;span class="s"&gt;kafka_num_consumers = 4;&lt;/span&gt;

&lt;span class="s"&gt;CREATE MATERIALIZED VIEW user_events_queue_mv&lt;/span&gt;
&lt;span class="s"&gt;TO user_events&lt;/span&gt;
&lt;span class="s"&gt;AS SELECT&lt;/span&gt;
    &lt;span class="s"&gt;user_id,&lt;/span&gt;
    &lt;span class="s"&gt;event_type,&lt;/span&gt;
    &lt;span class="s"&gt;timestamp,&lt;/span&gt;
    &lt;span class="s"&gt;parseJSON(properties) as properties&lt;/span&gt;
&lt;span class="s"&gt;FROM user_events_queue;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: apply compression and TTL for cold data&lt;/span&gt;
&lt;span class="c1"&gt;-- This alone can cut storage costs 3-4x vs Rockset&lt;/span&gt;

&lt;span class="c1"&gt;-- Change compression for specific columns&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_events&lt;/span&gt;
    &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Add TTL for automatic data lifecycle management&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_events&lt;/span&gt;
    &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="n"&gt;TTL&lt;/span&gt;
    &lt;span class="n"&gt;ingestion_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toStartOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingestion_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Clear blob data after 30 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://embeddable.com/blog/best-databases-for-analytics" rel="noopener noreferrer"&gt;Embeddable's 2026 review&lt;/a&gt; lists ClickHouse as the top choice for cost-sensitive embedded analytics. The reasoning is simple: your customers don’t want to pay for your database over-provisioning.&lt;/p&gt;

&lt;p&gt;Here’s what determines whether your ClickHouse vs Rockset cost comparison ends well.&lt;/p&gt;

&lt;p&gt;Most teams over-provision. ClickHouse runs fine on 2 vCPUs for many workloads. Start small and scale up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In my experience&lt;/strong&gt;, 80% of production ClickHouse deployments I’ve audited have oversized machines by at least 2x.&lt;/p&gt;

&lt;p&gt;Data value decays over time. Old logs. Old event data. Don’t pay premium storage for it.&lt;/p&gt;

&lt;p&gt;ClickHouse supports row-level TTLs and table-level TTLs. Rockset doesn’t have native lifecycle management at the same granularity.&lt;/p&gt;

&lt;p&gt;ClickHouse performs best with batch inserts of 100K-1M rows. Avoid single-row inserts. They create tiny parts and degrade read performance.&lt;/p&gt;

&lt;p&gt;Here’s a batch insert pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;clickhouse_driver&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clickhouse_driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events_batch&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO events (user_id, event_type, timestamp) VALUES&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;types_check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;columnar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rockset’s billing is opaque. ClickHouse provides system tables that show exact query costs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Monitor expensive queries in ClickHouse&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_duration_ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'QueryFinish'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;read_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000000000&lt;/span&gt;  &lt;span class="c1"&gt;-- Queries reading &amp;gt;1GB&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;read_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=33564256" rel="noopener noreferrer"&gt;Hacker News discussions&lt;/a&gt; consistently show teams discovering 3-5x cost overruns because they didn’t monitor query behavior.&lt;/p&gt;

&lt;p&gt;Let me walk you through three problem scenarios I’ve seen.&lt;/p&gt;

&lt;p&gt;Rockset’s on-demand model means query spikes cost you directly. ClickHouse absorbs spikes better because you provision for the base load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; For ClickHouse, use tiered storage. Hot data on NVMe. Warm data on SSDs. Cold data on object storage. Query hot data first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse tiered storage policy&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_events&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;DateTime64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt;
    &lt;span class="n"&gt;storage_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'tiered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cold_data_volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'s3_cold_storage'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Rockset, use auto-scaling policies but set hard cost caps. The platform will throttle before it blows your budget.&lt;/p&gt;

&lt;p&gt;Rockset indexes every field. A nested JSON object with 500 keys becomes 500 indexed fields. Plus the document overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pre-process your data. Flatten nested objects. Remove unnecessary fields. Rockset charges per indexed field in the storage tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In my experience&lt;/strong&gt;, a client reduced Rockset storage 40% just by removing debug fields before ingestion. The engineer spent 2 days on the filter. Saved $3,500/month.&lt;/p&gt;

&lt;p&gt;Rockset uses proprietary SQL extensions. Migrating away requires schema rewriting. ClickHouse uses standard ANSI SQL with extensions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.explo.co/blog/rockset-exploring-alternatives-for-dynamo-db-users" rel="noopener noreferrer"&gt;Explo's guide to alternatives&lt;/a&gt; notes that teams migrating from Rockset often find ClickHouse easier to adopt because of SQL compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; If you start with Rockset, wrap query logic in an abstraction layer. Use views or a query service that can switch backends.&lt;/p&gt;

&lt;p&gt;Yes, by a significant margin. Multiple benchmarks show ClickHouse at 30-50% the cost of Rockset for equivalent workloads. The gap widens with larger datasets.&lt;/p&gt;

&lt;p&gt;For single-row lookups and full-text search, yes. For aggregated analytics, ClickHouse is 2-5x faster. The performance difference depends entirely on your query pattern.&lt;/p&gt;

&lt;p&gt;Compute units on idle, data egress fees, and storage for indexed data that isn't queried. These can add 40-60% to your base list price.&lt;/p&gt;

&lt;p&gt;Yes, with a CDC pipeline. Use tools like Kafka Connect to replicate Rockset data to ClickHouse in real-time. Then switch query traffic gradually.&lt;/p&gt;

&lt;p&gt;ClickHouse achieves 3-10x compression on analytical workloads. Rockset typically achieves 1.5-3x. That means ClickHouse stores 60-80% less data for the same raw volume.&lt;/p&gt;

&lt;p&gt;Rockset for pure streaming with zero ETL. ClickHouse for streaming with batch optimization. ClickHouse’s cost advantage grows if you can tolerate 1-5 second ingestion delays.&lt;/p&gt;

&lt;p&gt;Yes. Rockset’s auto-schema detection and SQL-over-API simplify initial setup. ClickHouse requires more upfront configuration but offers more long-term control.&lt;/p&gt;

&lt;p&gt;Yes. Some teams use Rockset for real-time search and ClickHouse for historical analytics. This increases infrastructure complexity but optimizes for each query pattern.&lt;/p&gt;

&lt;p&gt;The ClickHouse vs Rockset cost debate comes down to one question: how much are you willing to pay for convenience?&lt;/p&gt;

&lt;p&gt;Rockset removes friction at ingestion time. ClickHouse removes friction at query time. If your queries are predictable and your data volume is growing, ClickHouse wins on cost.&lt;/p&gt;

&lt;p&gt;My team at SIVARO regularly migrates teams from Rockset to ClickHouse. The savings are consistent: 40-70% lower infrastructure costs with equivalent or better query performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your next move:&lt;/strong&gt; Start a proof-of-concept with ClickHouse. Load a representative dataset. Run your top 10 queries. Compare the bill. You’ll see the difference within a week.&lt;/p&gt;

&lt;p&gt;If you want hands-on help, message me. I’ve seen these migration patterns more times than I can count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nishaant Dixit&lt;/strong&gt; is the founder of SIVARO, a product engineering company specializing in data infrastructure and production AI systems. Since 2018, Nishaant has built systems processing 200K events per second and migrated over 2 petabytes of data between analytical platforms. He writes about the real costs of data engineering — not the marketing. Connect on &lt;a href="https://www.linkedin.com/in/nishaant-veer-dixit" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/comparison/rockset" rel="noopener noreferrer"&gt;Migrate from Rockset to ClickHouse — ClickHouse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.influxdata.com/comparison/clickhouse-vs-rockset/" rel="noopener noreferrer"&gt;ClickHouse vs Rockset — InfluxData&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://imply.io/blog/druid-nails-cost-efficiency-challenge-against-clickhouse-and-rockset/" rel="noopener noreferrer"&gt;Druid Nails Cost Efficiency Challenge Against ClickHouse — Imply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/blog/clickhouse-nails-cost-efficiency-challenge-against-druid-rockset" rel="noopener noreferrer"&gt;ClickHouse Nails Cost-Efficiency Challenge Against Druid — Altinity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.trustradius.com/compare-products/clickhouse-vs-rockset" rel="noopener noreferrer"&gt;ClickHouse vs Rockset User Reviews — TrustRadius&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clickhouse.com/blog/rockset-migration-to-clickhouse-lens" rel="noopener noreferrer"&gt;How Lens Made Its Database Faster and More Efficient — ClickHouse Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/rocksetcloud/rockset-beats-clickhouse-and-druid-on-the-star-schema-benchmark-ssb-8951b9e481e2" rel="noopener noreferrer"&gt;Rockset Beats ClickHouse on Star Schema Benchmark — Rockset Blog (Medium)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tinybird.co/blog/migrating-from-rockset-feature-comparison" rel="noopener noreferrer"&gt;Migrating from Rockset? Tinybird Feature Comparison — Tinybird&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://embeddable.com/blog/best-databases-for-analytics" rel="noopener noreferrer"&gt;Best Databases for Embedded Analytics 2026 — Embeddable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.explo.co/blog/rockset-exploring-alternatives-for-dynamo-db-users" rel="noopener noreferrer"&gt;Exploring Alternatives for Dynamo DB Users — Explo&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sivaro.in/articles/clickhouse-vs-rockset-cost-the-real-price-of-real-time" rel="noopener noreferrer"&gt;https://sivaro.in/articles/clickhouse-vs-rockset-cost-the-real-price-of-real-time&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
