<?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>Forem: Mohamed Hussain S</title>
    <description>The latest articles on Forem by Mohamed Hussain S (@mohhddhassan).</description>
    <link>https://forem.com/mohhddhassan</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%2F3228500%2Fc2e87a6e-70e3-4023-a3d3-5adddef072f2.jpeg</url>
      <title>Forem: Mohamed Hussain S</title>
      <link>https://forem.com/mohhddhassan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mohhddhassan"/>
    <language>en</language>
    <item>
      <title>Why Too Many Parts Hurt ClickHouse Performance</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Mon, 25 May 2026 14:00:25 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/why-too-many-parts-hurt-clickhouse-performance-4c7n</link>
      <guid>https://forem.com/mohhddhassan/why-too-many-parts-hurt-clickhouse-performance-4c7n</guid>
      <description>&lt;p&gt;A lot of people initially think ClickHouse performance problems come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large queries&lt;/li&gt;
&lt;li&gt;bad joins&lt;/li&gt;
&lt;li&gt;massive datasets&lt;/li&gt;
&lt;li&gt;missing indexes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, those things &lt;em&gt;can&lt;/em&gt; matter.&lt;/p&gt;

&lt;p&gt;But one of the most common operational problems in ClickHouse often starts much earlier:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;too many tiny parts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one of those issues that usually stays invisible at first.&lt;/p&gt;

&lt;p&gt;Then suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merges fall behind&lt;/li&gt;
&lt;li&gt;queries slow down&lt;/li&gt;
&lt;li&gt;memory usage increases&lt;/li&gt;
&lt;li&gt;inserts become unstable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the cluster starts behaving strangely.&lt;/p&gt;




&lt;h1&gt;
  
  
  Every Insert Creates Parts
&lt;/h1&gt;

&lt;p&gt;This is the first thing that’s important to understand.&lt;/p&gt;

&lt;p&gt;In MergeTree-based engines, ClickHouse stores data as immutable parts.&lt;/p&gt;

&lt;p&gt;Something as simple as:&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;creates new parts on disk.&lt;/p&gt;

&lt;p&gt;And this is completely normal.&lt;/p&gt;

&lt;p&gt;ClickHouse is designed around this storage model.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;parts themselves are not the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real issue starts when parts begin accumulating faster than merges can stabilize them.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Tiny Inserts Become Dangerous
&lt;/h1&gt;

&lt;p&gt;At smaller scale, tiny inserts may seem harmless.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserting row-by-row&lt;/li&gt;
&lt;li&gt;extremely frequent micro-batches&lt;/li&gt;
&lt;li&gt;tiny streaming flush intervals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initially:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;everything still works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But over time, the number of parts starts growing aggressively.&lt;/p&gt;

&lt;p&gt;Now ClickHouse has to manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more metadata&lt;/li&gt;
&lt;li&gt;more merges&lt;/li&gt;
&lt;li&gt;more scheduling&lt;/li&gt;
&lt;li&gt;more file operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates operational overhead.&lt;/p&gt;

&lt;p&gt;Meaning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the system starts spending increasing resources managing fragmentation itself.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Why Merges Matter So Much
&lt;/h1&gt;

&lt;p&gt;ClickHouse relies heavily on background merges.&lt;/p&gt;

&lt;p&gt;These merges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;combine smaller parts&lt;/li&gt;
&lt;li&gt;reduce fragmentation&lt;/li&gt;
&lt;li&gt;improve compression&lt;/li&gt;
&lt;li&gt;optimize query performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under healthy ingestion patterns, merges naturally keep the system stable over time.&lt;/p&gt;

&lt;p&gt;That is the ideal state.&lt;/p&gt;

&lt;p&gt;But problems start when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parts created per second
        &amp;gt;
parts merged per second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now fragmented parts begin accumulating faster than ClickHouse can compact them.&lt;/p&gt;

&lt;p&gt;And this is usually where instability slowly starts building.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Dangerous Part Is That It Builds Slowly
&lt;/h1&gt;

&lt;p&gt;This is what makes the issue tricky operationally.&lt;/p&gt;

&lt;p&gt;You usually do not notice the problem immediately.&lt;/p&gt;

&lt;p&gt;The cluster may look perfectly healthy initially.&lt;/p&gt;

&lt;p&gt;Then gradually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;insert latency increases&lt;/li&gt;
&lt;li&gt;merges lag behind&lt;/li&gt;
&lt;li&gt;CPU usage becomes unstable&lt;/li&gt;
&lt;li&gt;queries become heavier&lt;/li&gt;
&lt;li&gt;replication slows down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And eventually ClickHouse may start throwing errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Too many parts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the merge system is already under serious pressure.&lt;/p&gt;




&lt;h1&gt;
  
  
  Queries Also Become More Expensive
&lt;/h1&gt;

&lt;p&gt;A lot of people think parts only affect inserts.&lt;/p&gt;

&lt;p&gt;But queries suffer too.&lt;/p&gt;

&lt;p&gt;Because queries now need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open more parts&lt;/li&gt;
&lt;li&gt;scan more metadata&lt;/li&gt;
&lt;li&gt;coordinate more files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when the actual dataset itself is not massive.&lt;/p&gt;

&lt;p&gt;So sometimes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;performance degradation comes more from fragmentation than raw data volume.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a very important operational insight.&lt;/p&gt;




&lt;h1&gt;
  
  
  FINAL Does Not Really Solve This
&lt;/h1&gt;

&lt;p&gt;One thing that’s important to understand:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FINAL&lt;/code&gt; is not really a solution for too many parts.&lt;/p&gt;

&lt;p&gt;For example:&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;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;FINAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FINAL&lt;/code&gt; applies merge logic during query execution.&lt;/p&gt;

&lt;p&gt;But the fragmented parts still physically exist underneath.&lt;/p&gt;

&lt;p&gt;So if the system already has excessive fragmentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queries still scan many parts&lt;/li&gt;
&lt;li&gt;merge pressure still exists&lt;/li&gt;
&lt;li&gt;query execution can become heavier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FINAL can actually become more expensive when fragmentation becomes unhealthy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real fix is usually improving ingestion and merge behavior itself.&lt;/p&gt;




&lt;h1&gt;
  
  
  Over-Partitioning Can Quietly Make This Worse
&lt;/h1&gt;

&lt;p&gt;Another thing that often accelerates part explosion is overly granular partitioning.&lt;/p&gt;

&lt;p&gt;For example:&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;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMMDDhh&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;instead of something broader like:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now even small inserts may create parts across many partitions simultaneously.&lt;/p&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a single insert can end up creating multiple fragmented parts underneath.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And over time, merge pressure increases much faster than expected.&lt;/p&gt;




&lt;h1&gt;
  
  
  ClickHouse Also Has Ways to Help
&lt;/h1&gt;

&lt;p&gt;Modern ClickHouse versions also support features like async inserts to help reduce excessive tiny-part creation.&lt;/p&gt;

&lt;p&gt;Instead of immediately flushing every small insert into separate parts, ClickHouse can buffer inserts internally before writing larger parts to disk.&lt;/p&gt;

&lt;p&gt;This helps reduce fragmentation and merge pressure in workloads that naturally produce smaller inserts.&lt;/p&gt;

&lt;p&gt;But async inserts are not a replacement for healthy ingestion patterns themselves.&lt;/p&gt;

&lt;p&gt;Stable batching still matters a lot.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Batch Size Matters So Much
&lt;/h1&gt;

&lt;p&gt;ClickHouse generally performs much better with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger batches&lt;/li&gt;
&lt;li&gt;fewer inserts&lt;/li&gt;
&lt;li&gt;healthier merge behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because fewer parts means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer merges&lt;/li&gt;
&lt;li&gt;lower metadata overhead&lt;/li&gt;
&lt;li&gt;better compression&lt;/li&gt;
&lt;li&gt;more efficient scans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the reasons ClickHouse ingestion patterns often look very different from traditional OLTP systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Too Many Parts Also Affects Startup and Recovery
&lt;/h1&gt;

&lt;p&gt;Another thing people often discover late:&lt;/p&gt;

&lt;p&gt;Large numbers of parts also affect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;startup time&lt;/li&gt;
&lt;li&gt;replication recovery&lt;/li&gt;
&lt;li&gt;metadata loading&lt;/li&gt;
&lt;li&gt;server restarts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because ClickHouse now has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scan part metadata&lt;/li&gt;
&lt;li&gt;validate parts&lt;/li&gt;
&lt;li&gt;rebuild internal state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;before the server becomes fully operational again.&lt;/p&gt;

&lt;p&gt;So the issue is not just query performance.&lt;/p&gt;

&lt;p&gt;It becomes an overall operational stability problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Important Lesson
&lt;/h1&gt;

&lt;p&gt;One thing I’ve noticed with ClickHouse is that many performance problems are actually merge-management problems underneath.&lt;/p&gt;

&lt;p&gt;And too many parts is one of the clearest examples of that.&lt;/p&gt;

&lt;p&gt;Because the issue usually is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“ClickHouse cannot handle large data.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The issue is more often:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fragmentation and merge pressure slowly became unhealthy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a very different operational problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;ClickHouse is extremely good at handling massive analytical workloads.&lt;/p&gt;

&lt;p&gt;But it performs best when the storage engine is allowed to merge parts efficiently.&lt;/p&gt;

&lt;p&gt;And sometimes the biggest performance problem is not the query itself.&lt;/p&gt;

&lt;p&gt;It is the thousands of tiny fragmented parts quietly building underneath the system over time.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>database</category>
      <category>dataengineering</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Real-Time Analytics Eventually Changes Your Database Architecture</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Tue, 19 May 2026 16:36:19 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/why-real-time-analytics-eventually-changes-your-database-architecture-7gf</link>
      <guid>https://forem.com/mohhddhassan/why-real-time-analytics-eventually-changes-your-database-architecture-7gf</guid>
      <description>&lt;p&gt;A lot of systems begin with a single database.&lt;/p&gt;

&lt;p&gt;Usually PostgreSQL.&lt;/p&gt;

&lt;p&gt;And honestly, in the beginning, that works perfectly fine.&lt;/p&gt;

&lt;p&gt;The application stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;li&gt;payments&lt;/li&gt;
&lt;li&gt;inventory&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;operational state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dashboards query the same database.&lt;/p&gt;

&lt;p&gt;Analytics queries also run directly on PostgreSQL.&lt;/p&gt;

&lt;p&gt;Everything feels simple.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problem Usually Starts Slowly
&lt;/h1&gt;

&lt;p&gt;At first, analytical queries are small.&lt;/p&gt;

&lt;p&gt;Maybe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;daily reports&lt;/li&gt;
&lt;li&gt;lightweight aggregations&lt;/li&gt;
&lt;li&gt;small dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing too serious.&lt;/p&gt;

&lt;p&gt;But over time, systems start generating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more events&lt;/li&gt;
&lt;li&gt;more metrics&lt;/li&gt;
&lt;li&gt;more logs&lt;/li&gt;
&lt;li&gt;more historical records&lt;/li&gt;
&lt;li&gt;more observability data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And analytical workloads start behaving very differently from transactional workloads.&lt;/p&gt;

&lt;p&gt;For example:&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;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;service_name&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;response_time_ms&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;metrics&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;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;service_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a very different kind of workload from:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One is trying to preserve operational correctness.&lt;/p&gt;

&lt;p&gt;The other is trying to analyze huge amounts of historical data.&lt;/p&gt;

&lt;p&gt;And eventually those workloads start colliding.&lt;/p&gt;




&lt;h1&gt;
  
  
  PostgreSQL Slowly Becomes Responsible for Everything
&lt;/h1&gt;

&lt;p&gt;This is where things usually start getting interesting.&lt;/p&gt;

&lt;p&gt;A lot of systems unintentionally turn PostgreSQL into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the transactional database&lt;/li&gt;
&lt;li&gt;the reporting database&lt;/li&gt;
&lt;li&gt;the analytics database&lt;/li&gt;
&lt;li&gt;the observability database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;all at the same time.&lt;/p&gt;

&lt;p&gt;And honestly, modern PostgreSQL is capable enough that this can work surprisingly well for a while.&lt;/p&gt;

&lt;p&gt;Until:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dashboards become heavier&lt;/li&gt;
&lt;li&gt;retention windows grow&lt;/li&gt;
&lt;li&gt;analytical scans become larger&lt;/li&gt;
&lt;li&gt;observability traffic increases&lt;/li&gt;
&lt;li&gt;aggregations become expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now suddenly the same database handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payments&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;li&gt;inventory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;is also handling large analytical workloads.&lt;/p&gt;

&lt;p&gt;And this is usually where architectural pressure starts building.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Real Problem Is Workload Isolation
&lt;/h1&gt;

&lt;p&gt;This is honestly the biggest lesson.&lt;/p&gt;

&lt;p&gt;The issue is usually not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“PostgreSQL is slow.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The issue is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;transactional workloads and analytical workloads optimize for completely different things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Transactional systems care heavily about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;li&gt;operational latency&lt;/li&gt;
&lt;li&gt;updates&lt;/li&gt;
&lt;li&gt;row-level modifications&lt;/li&gt;
&lt;li&gt;business correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analytical systems care heavily about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large scans&lt;/li&gt;
&lt;li&gt;aggregations&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;historical analytics&lt;/li&gt;
&lt;li&gt;query throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are fundamentally different workload patterns.&lt;/p&gt;

&lt;p&gt;And eventually trying to optimize one database perfectly for both becomes painful.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Observability Changes Everything So Quickly
&lt;/h1&gt;

&lt;p&gt;One thing I find interesting is how fast observability workloads expose architectural limitations.&lt;/p&gt;

&lt;p&gt;Because observability systems continuously generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;traces&lt;/li&gt;
&lt;li&gt;events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And these workloads grow aggressively over time.&lt;/p&gt;

&lt;p&gt;Now imagine running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large aggregations&lt;/li&gt;
&lt;li&gt;historical scans&lt;/li&gt;
&lt;li&gt;high-cardinality queries&lt;/li&gt;
&lt;li&gt;real-time dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;on the same database handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;inventory&lt;/li&gt;
&lt;li&gt;operational business logic&lt;/li&gt;
&lt;li&gt;transactional traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At smaller scale this may still work.&lt;/p&gt;

&lt;p&gt;At larger scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query contention increases&lt;/li&gt;
&lt;li&gt;operational latency becomes sensitive&lt;/li&gt;
&lt;li&gt;workload isolation becomes harder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And eventually systems start evolving toward separation.&lt;/p&gt;




&lt;h1&gt;
  
  
  This Is Usually When Analytical Databases Start Appearing
&lt;/h1&gt;

&lt;p&gt;At some point, many systems evolve toward something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
PostgreSQL
    ↓
CDC / Kafka / Airbyte
    ↓
ClickHouse / OLAP DB
    ↓
Analytics / Dashboards / Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has become extremely common in modern analytical systems.&lt;/p&gt;

&lt;p&gt;And honestly, the reason is pretty simple:&lt;/p&gt;

&lt;p&gt;PostgreSQL remains responsible for operational correctness.&lt;/p&gt;

&lt;p&gt;ClickHouse becomes responsible for analytical scale.&lt;/p&gt;

&lt;p&gt;Each system handles the workload it was actually designed for.&lt;/p&gt;




&lt;h1&gt;
  
  
  Not All Analytical Data Needs PostgreSQL First
&lt;/h1&gt;

&lt;p&gt;One important thing though:&lt;/p&gt;

&lt;p&gt;Not all analytical data even originates from PostgreSQL.&lt;/p&gt;

&lt;p&gt;A lot of observability workloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;traces&lt;/li&gt;
&lt;li&gt;telemetry events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;often flow directly into ClickHouse/OLAP DB through streaming pipelines.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Applications / Services
        ↓
Kafka / Streaming Pipelines
        ↓
ClickHouse / OLAP DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many systems, PostgreSQL stores the business data while ClickHouse directly handles logs, metrics, events, and analytical workloads.&lt;/p&gt;

&lt;p&gt;And honestly, this makes a lot of sense.&lt;/p&gt;

&lt;p&gt;Because analytical systems are usually optimized for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;append-heavy ingestion&lt;/li&gt;
&lt;li&gt;historical querying&lt;/li&gt;
&lt;li&gt;event-style workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;not transactional business operations.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Not Just Use ClickHouse for Everything?
&lt;/h1&gt;

&lt;p&gt;This is another common misunderstanding.&lt;/p&gt;

&lt;p&gt;ClickHouse is incredible for analytical workloads.&lt;/p&gt;

&lt;p&gt;But transactional systems still require things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frequent updates&lt;/li&gt;
&lt;li&gt;operational consistency&lt;/li&gt;
&lt;li&gt;transactional guarantees&lt;/li&gt;
&lt;li&gt;row-level modifications&lt;/li&gt;
&lt;li&gt;business-critical correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the primary design goals of analytical databases.&lt;/p&gt;

&lt;p&gt;You generally do not want your:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication system&lt;/li&gt;
&lt;li&gt;payment workflows&lt;/li&gt;
&lt;li&gt;inventory updates&lt;/li&gt;
&lt;li&gt;operational application state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;depending entirely on analytical database behavior.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why CDC Pipelines Become So Important
&lt;/h1&gt;

&lt;p&gt;One reason this architecture became so practical is CDC (Change Data Capture).&lt;/p&gt;

&lt;p&gt;Instead of repeatedly exporting data manually, systems continuously stream changes from PostgreSQL into analytical systems using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;Debezium&lt;/li&gt;
&lt;li&gt;Airbyte&lt;/li&gt;
&lt;li&gt;streaming pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operational systems continue working normally&lt;/li&gt;
&lt;li&gt;analytical systems receive near real-time data&lt;/li&gt;
&lt;li&gt;workloads stay separated cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And analytical queries no longer compete directly against transactional traffic.&lt;/p&gt;




&lt;h1&gt;
  
  
  Don’t Rush Into Multi-Database Architectures
&lt;/h1&gt;

&lt;p&gt;One important thing though:&lt;/p&gt;

&lt;p&gt;Most systems do not need Kafka + ClickHouse pipelines on Day 1.&lt;/p&gt;

&lt;p&gt;Honestly, many applications can scale surprisingly far with PostgreSQL alone using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;proper indexing&lt;/li&gt;
&lt;li&gt;query optimization&lt;/li&gt;
&lt;li&gt;read replicas&lt;/li&gt;
&lt;li&gt;partitioning&lt;/li&gt;
&lt;li&gt;extensions like &lt;a href="https://github.com/citusdata/citus" rel="noopener noreferrer"&gt;Citus&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to introduce more infrastructure as early as possible.&lt;/p&gt;

&lt;p&gt;The real signal usually appears when analytical workloads start affecting operational user experience.&lt;/p&gt;

&lt;p&gt;That is often when workload separation starts becoming worth the additional architectural complexity.&lt;/p&gt;

&lt;p&gt;Because systems like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDC pipelines&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;analytical databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;also introduce operational overhead of their own.&lt;/p&gt;

&lt;p&gt;And good architecture is usually about introducing complexity only when the workload actually demands it.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Bigger Engineering Lesson
&lt;/h1&gt;

&lt;p&gt;Most systems do not start with multiple databases.&lt;/p&gt;

&lt;p&gt;They evolve into them as workloads grow.&lt;/p&gt;

&lt;p&gt;Transactional workloads and analytical workloads behave very differently at scale.&lt;/p&gt;

&lt;p&gt;And eventually systems start separating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operational correctness&lt;/li&gt;
&lt;li&gt;analytical querying&lt;/li&gt;
&lt;li&gt;observability workloads&lt;/li&gt;
&lt;li&gt;historical analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;into infrastructure optimized for each workload.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;A lot of modern systems do not start with multiple databases.&lt;/p&gt;

&lt;p&gt;They evolve into them.&lt;/p&gt;

&lt;p&gt;Because transactional workloads and analytical workloads eventually want very different things from the same infrastructure.&lt;/p&gt;

&lt;p&gt;And real-time analytics is often the thing that forces that architectural separation to happen.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>postgres</category>
      <category>dataengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>FINAL in ClickHouse Isn’t as Expensive as It Used to Be</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Thu, 14 May 2026 16:04:00 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/final-in-clickhouse-isnt-as-expensive-as-it-used-to-be-1ne9</link>
      <guid>https://forem.com/mohhddhassan/final-in-clickhouse-isnt-as-expensive-as-it-used-to-be-1ne9</guid>
      <description>&lt;p&gt;For a long time, the advice around &lt;code&gt;FINAL&lt;/code&gt; in ClickHouse was pretty straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Avoid it whenever possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And honestly, that advice existed for good reasons.&lt;/p&gt;

&lt;p&gt;Older versions of ClickHouse could make &lt;code&gt;FINAL&lt;/code&gt; extremely expensive depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table size&lt;/li&gt;
&lt;li&gt;partitioning&lt;/li&gt;
&lt;li&gt;number of parts&lt;/li&gt;
&lt;li&gt;merge state&lt;/li&gt;
&lt;li&gt;query patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So people started treating &lt;code&gt;FINAL&lt;/code&gt; almost like a red flag.&lt;/p&gt;

&lt;p&gt;But modern ClickHouse has changed a lot.&lt;/p&gt;

&lt;p&gt;And I think the conversation around &lt;code&gt;FINAL&lt;/code&gt; deserves a bit more nuance now.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why FINAL Existed in the First Place
&lt;/h1&gt;

&lt;p&gt;To understand why &lt;code&gt;FINAL&lt;/code&gt; was historically considered expensive, you first need to understand what it actually does.&lt;/p&gt;

&lt;p&gt;In engines like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ReplacingMergeTree&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CollapsingMergeTree&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VersionedCollapsingMergeTree&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse does not immediately rewrite rows in place.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserts create new parts&lt;/li&gt;
&lt;li&gt;background merges reconcile rows later&lt;/li&gt;
&lt;li&gt;deduplication happens asynchronously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means queries can temporarily see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate versions&lt;/li&gt;
&lt;li&gt;old versions&lt;/li&gt;
&lt;li&gt;intermediate states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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;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;users&lt;/span&gt;
&lt;span class="k"&gt;FINAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FINAL&lt;/code&gt; forces ClickHouse to apply merge logic during query execution itself.&lt;/p&gt;

&lt;p&gt;That means the query may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read more data&lt;/li&gt;
&lt;li&gt;perform additional deduplication work&lt;/li&gt;
&lt;li&gt;consume more CPU and memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why older advice strongly discouraged using it everywhere.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Old FINAL Problem
&lt;/h1&gt;

&lt;p&gt;Historically, &lt;code&gt;FINAL&lt;/code&gt; could become painful on large datasets.&lt;/p&gt;

&lt;p&gt;Especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;partitions were large&lt;/li&gt;
&lt;li&gt;too many parts existed&lt;/li&gt;
&lt;li&gt;merges lagged behind&lt;/li&gt;
&lt;li&gt;queries scanned massive ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People would add:&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;FINAL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to "fix" duplicate rows without understanding why duplicates existed in the first place.&lt;/p&gt;

&lt;p&gt;The result was often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slower queries&lt;/li&gt;
&lt;li&gt;higher memory usage&lt;/li&gt;
&lt;li&gt;unnecessary query overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the community advice became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Design your schema properly and avoid FINAL whenever possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;That advice still matters.&lt;/p&gt;

&lt;p&gt;But the implementation of &lt;code&gt;FINAL&lt;/code&gt; itself has improved significantly over time.&lt;/p&gt;




&lt;h1&gt;
  
  
  Modern ClickHouse Has Improved FINAL a Lot
&lt;/h1&gt;

&lt;p&gt;Recent ClickHouse versions introduced multiple improvements around &lt;code&gt;FINAL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parallel execution&lt;/li&gt;
&lt;li&gt;partition-aware optimizations&lt;/li&gt;
&lt;li&gt;improved memory behavior&lt;/li&gt;
&lt;li&gt;smarter merge execution&lt;/li&gt;
&lt;li&gt;reduced unnecessary reads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;FINAL&lt;/code&gt; is no longer the monster it used to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this is important because newer ClickHouse guidance has also become more practical about using it when necessary.&lt;/p&gt;

&lt;p&gt;Even in some recent discussions and office hours from the ClickHouse ecosystem, using &lt;code&gt;FINAL&lt;/code&gt; for latest-state queries is no longer treated as automatically wrong.&lt;/p&gt;

&lt;p&gt;That would have sounded controversial a few years ago.&lt;/p&gt;




&lt;h1&gt;
  
  
  FINAL vs argMax Isn’t Always a Simple Comparison
&lt;/h1&gt;

&lt;p&gt;For a long time, many ClickHouse users avoided &lt;code&gt;FINAL&lt;/code&gt; by using patterns like:&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;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;argMax&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="k"&gt;version&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;users&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;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And honestly, for older ClickHouse versions and large workloads, that often made sense.&lt;/p&gt;

&lt;p&gt;But modern ClickHouse has improved &lt;code&gt;FINAL&lt;/code&gt; significantly enough that the tradeoff is no longer as one-sided as it used to be.&lt;/p&gt;

&lt;p&gt;In some latest-state query scenarios, using &lt;code&gt;FINAL&lt;/code&gt; can now be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simpler&lt;/li&gt;
&lt;li&gt;easier to maintain&lt;/li&gt;
&lt;li&gt;and completely reasonable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table size&lt;/li&gt;
&lt;li&gt;partitioning&lt;/li&gt;
&lt;li&gt;query filters&lt;/li&gt;
&lt;li&gt;merge behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is understanding the workload instead of blindly following older rules.&lt;/p&gt;




&lt;h1&gt;
  
  
  So… Is FINAL Safe to Use Now?
&lt;/h1&gt;

&lt;p&gt;This is where nuance matters.&lt;/p&gt;

&lt;p&gt;The answer is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"FINAL bad"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and also not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"FINAL free now"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FINAL is much more practical in modern ClickHouse, but workload design still matters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where FINAL Makes Sense
&lt;/h1&gt;

&lt;p&gt;There are legitimate cases where &lt;code&gt;FINAL&lt;/code&gt; is completely reasonable now.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latest-state queries&lt;/li&gt;
&lt;li&gt;smaller partitions&lt;/li&gt;
&lt;li&gt;low-latency analytical workloads&lt;/li&gt;
&lt;li&gt;deduplicated views over mutable datasets&lt;/li&gt;
&lt;li&gt;operational analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially when using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;proper partitioning&lt;/li&gt;
&lt;li&gt;controlled part counts&lt;/li&gt;
&lt;li&gt;optimized schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, modern ClickHouse handles &lt;code&gt;FINAL&lt;/code&gt; much better than older versions did.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where FINAL Can Still Hurt
&lt;/h1&gt;

&lt;p&gt;Even with improvements, &lt;code&gt;FINAL&lt;/code&gt; is not magically free.&lt;/p&gt;

&lt;p&gt;It can still become expensive when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning huge datasets&lt;/li&gt;
&lt;li&gt;querying many partitions&lt;/li&gt;
&lt;li&gt;merges are heavily delayed&lt;/li&gt;
&lt;li&gt;part counts explode&lt;/li&gt;
&lt;li&gt;schema design is poor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&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;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;massive_events_table&lt;/span&gt;
&lt;span class="k"&gt;FINAL&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;30&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;On very large analytical datasets, this can still force substantial extra work.&lt;/p&gt;

&lt;p&gt;So blindly adding &lt;code&gt;FINAL&lt;/code&gt; everywhere is still not a great idea.&lt;/p&gt;




&lt;h1&gt;
  
  
  SELECT ... FINAL vs OPTIMIZE TABLE ... FINAL
&lt;/h1&gt;

&lt;p&gt;One important distinction:&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;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;users&lt;/span&gt; &lt;span class="k"&gt;FINAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&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;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;FINAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are completely different operations.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT ... FINAL&lt;/code&gt; applies merge logic during query execution.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OPTIMIZE TABLE ... FINAL&lt;/code&gt; forces a heavy merge operation on storage parts themselves.&lt;/p&gt;

&lt;p&gt;The first is a query-time behavior.&lt;/p&gt;

&lt;p&gt;The second is a storage-level operation that can become extremely expensive on large datasets.&lt;/p&gt;

&lt;p&gt;People often mix these two together when discussing FINAL performance, but they solve very different problems.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Bigger Lesson Is Understanding Why You Need FINAL
&lt;/h1&gt;

&lt;p&gt;This is honestly the most important part.&lt;/p&gt;

&lt;p&gt;A lot of people use &lt;code&gt;FINAL&lt;/code&gt; reactively.&lt;/p&gt;

&lt;p&gt;They see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate rows&lt;/li&gt;
&lt;li&gt;outdated versions&lt;/li&gt;
&lt;li&gt;inconsistent query results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and immediately add:&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;FINAL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merge behavior&lt;/li&gt;
&lt;li&gt;part lifecycle&lt;/li&gt;
&lt;li&gt;asynchronous deduplication&lt;/li&gt;
&lt;li&gt;storage engine behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That usually creates larger problems later.&lt;/p&gt;

&lt;p&gt;The better approach is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Understand why the table requires FINAL in the first place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because sometimes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the schema can improve&lt;/li&gt;
&lt;li&gt;partitioning can improve&lt;/li&gt;
&lt;li&gt;merges can stabilize naturally&lt;/li&gt;
&lt;li&gt;query design can change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And sometimes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using FINAL is actually perfectly acceptable.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  ClickHouse Advice Evolves Too
&lt;/h1&gt;

&lt;p&gt;One thing I find interesting about ClickHouse is how quickly operational advice evolves as the engine improves.&lt;/p&gt;

&lt;p&gt;Advice that was absolutely correct for older versions can become incomplete later.&lt;/p&gt;

&lt;p&gt;And I think &lt;code&gt;FINAL&lt;/code&gt; is one of the best examples of that.&lt;/p&gt;

&lt;p&gt;Older guidance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;avoid FINAL aggressively&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern reality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;understand FINAL properly before deciding whether to avoid it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more useful mental model now.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;I still would not recommend blindly adding &lt;code&gt;FINAL&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;But I also do not think modern ClickHouse users should automatically treat it like a disaster anymore.&lt;/p&gt;

&lt;p&gt;The real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is FINAL bad?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why does this query need FINAL, and is that tradeoff acceptable for this workload?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mindset leads to much better ClickHouse designs than simply following old rules blindly.&lt;/p&gt;




&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://clickhouse.com/docs/sql-reference/statements/select/from#final-modifier" rel="noopener noreferrer"&gt;ClickHouse Docs - FINAL Modifier&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kb.altinity.com/altinity-kb-queries-and-syntax/altinity-kb-final-clause-speed/" rel="noopener noreferrer"&gt;Altinity KB - FINAL Clause Speed&lt;/a&gt;&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>dataengineering</category>
      <category>databaseopimization</category>
      <category>replacingmergetree</category>
    </item>
    <item>
      <title>Why PostgreSQL and ClickHouse Work So Well Together</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Mon, 11 May 2026 09:26:10 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/why-postgresql-and-clickhouse-work-so-well-together-5128</link>
      <guid>https://forem.com/mohhddhassan/why-postgresql-and-clickhouse-work-so-well-together-5128</guid>
      <description>&lt;p&gt;A lot of people compare PostgreSQL and ClickHouse like they are competing databases.&lt;/p&gt;

&lt;p&gt;They really are not.&lt;/p&gt;

&lt;p&gt;In fact, modern data systems often use both together.&lt;/p&gt;

&lt;p&gt;And once you understand what each database is optimized for, the reason becomes pretty obvious.&lt;/p&gt;




&lt;h1&gt;
  
  
  PostgreSQL and ClickHouse Solve Different Problems
&lt;/h1&gt;

&lt;p&gt;The biggest mistake people make is expecting both databases to behave similarly.&lt;/p&gt;

&lt;p&gt;They are built for entirely different workloads.&lt;/p&gt;

&lt;p&gt;PostgreSQL is primarily an OLTP database.&lt;/p&gt;

&lt;p&gt;ClickHouse is primarily an OLAP database.&lt;/p&gt;

&lt;p&gt;That single difference changes almost everything about how they think internally.&lt;/p&gt;




&lt;h1&gt;
  
  
  PostgreSQL Thinks About Transactions First
&lt;/h1&gt;

&lt;p&gt;PostgreSQL is extremely good at handling transactional workloads.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user data&lt;/li&gt;
&lt;li&gt;payments&lt;/li&gt;
&lt;li&gt;inventory&lt;/li&gt;
&lt;li&gt;banking records&lt;/li&gt;
&lt;li&gt;order systems&lt;/li&gt;
&lt;li&gt;application state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are systems where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistency matters&lt;/li&gt;
&lt;li&gt;updates happen frequently&lt;/li&gt;
&lt;li&gt;rows are modified constantly&lt;/li&gt;
&lt;li&gt;transactions must be reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of workload is where PostgreSQL shines.&lt;/p&gt;

&lt;p&gt;You want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACID guarantees&lt;/li&gt;
&lt;li&gt;reliable transactions&lt;/li&gt;
&lt;li&gt;row-level updates&lt;/li&gt;
&lt;li&gt;strong consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL is designed around exactly that.&lt;/p&gt;




&lt;h1&gt;
  
  
  ClickHouse Thinks About Analytics First
&lt;/h1&gt;

&lt;p&gt;ClickHouse approaches data very differently.&lt;/p&gt;

&lt;p&gt;Instead of optimizing for frequent row updates, it optimizes for analytical queries across massive datasets.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;event streams&lt;/li&gt;
&lt;li&gt;analytical dashboards&lt;/li&gt;
&lt;li&gt;time-series workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&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;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;service_name&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;response_time_ms&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;metrics&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="n"&gt;HOUR&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;service_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a completely different style of workload.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modifying small numbers of rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse is optimized for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning huge amounts of data efficiently&lt;/li&gt;
&lt;li&gt;aggregating billions of records&lt;/li&gt;
&lt;li&gt;compressing analytical datasets&lt;/li&gt;
&lt;li&gt;fast columnar reads&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  PostgreSQL Stores the Business. ClickHouse Explains It.
&lt;/h1&gt;

&lt;p&gt;This is honestly the simplest way I think about it now.&lt;/p&gt;

&lt;p&gt;PostgreSQL usually stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current application state&lt;/li&gt;
&lt;li&gt;transactional business data&lt;/li&gt;
&lt;li&gt;operational records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse usually stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;analytical history&lt;/li&gt;
&lt;li&gt;events&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;large-scale queryable telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One powers the application.&lt;/p&gt;

&lt;p&gt;The other explains what the application is doing.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why They Commonly Exist Together
&lt;/h1&gt;

&lt;p&gt;This is where things get interesting.&lt;/p&gt;

&lt;p&gt;In many modern architectures, PostgreSQL becomes the operational source of truth.&lt;/p&gt;

&lt;p&gt;Then data flows into ClickHouse for analytics.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
PostgreSQL
    ↓
CDC / Airbyte / Kafka
    ↓
ClickHouse
    ↓
Dashboards / Analytics / Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is far more common than many people realize.&lt;/p&gt;

&lt;p&gt;Because each database is doing what it is best at.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Not Just Use PostgreSQL for Analytics?
&lt;/h1&gt;

&lt;p&gt;PostgreSQL &lt;em&gt;can&lt;/em&gt; do analytical queries.&lt;/p&gt;

&lt;p&gt;But analytical workloads behave very differently from transactional workloads.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning billions of rows&lt;/li&gt;
&lt;li&gt;large aggregations&lt;/li&gt;
&lt;li&gt;observability queries&lt;/li&gt;
&lt;li&gt;real-time analytics&lt;/li&gt;
&lt;li&gt;historical trend analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These workloads stress databases differently.&lt;/p&gt;

&lt;p&gt;ClickHouse is optimized around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;columnar storage&lt;/li&gt;
&lt;li&gt;vectorized execution&lt;/li&gt;
&lt;li&gt;aggressive compression&lt;/li&gt;
&lt;li&gt;analytical query execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why queries over huge datasets often feel dramatically faster in ClickHouse.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Not Just Use ClickHouse for Everything?
&lt;/h1&gt;

&lt;p&gt;This is another common misunderstanding.&lt;/p&gt;

&lt;p&gt;ClickHouse is incredible for analytics.&lt;/p&gt;

&lt;p&gt;But transactional systems require things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frequent updates&lt;/li&gt;
&lt;li&gt;transactional consistency&lt;/li&gt;
&lt;li&gt;row-level modifications&lt;/li&gt;
&lt;li&gt;operational application state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not the primary design goal of ClickHouse.&lt;/p&gt;

&lt;p&gt;You generally do not want your:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user authentication system&lt;/li&gt;
&lt;li&gt;banking transactions&lt;/li&gt;
&lt;li&gt;inventory updates&lt;/li&gt;
&lt;li&gt;operational business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;to depend entirely on analytical database behavior.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Interesting Part Is the Separation of Responsibilities
&lt;/h1&gt;

&lt;p&gt;What I personally find interesting is how these systems complement each other instead of replacing each other.&lt;/p&gt;

&lt;p&gt;PostgreSQL handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operational correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;analytical scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation creates much cleaner architectures.&lt;/p&gt;

&lt;p&gt;Instead of forcing one database to solve every problem, each system handles the workload it was designed for.&lt;/p&gt;




&lt;h1&gt;
  
  
  CDC Is What Connects Them
&lt;/h1&gt;

&lt;p&gt;One thing that makes this architecture powerful is CDC (Change Data Capture).&lt;/p&gt;

&lt;p&gt;Instead of manually exporting data repeatedly, systems can stream changes from PostgreSQL into ClickHouse continuously.&lt;/p&gt;

&lt;p&gt;Tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debezium&lt;/li&gt;
&lt;li&gt;Airbyte&lt;/li&gt;
&lt;li&gt;Kafka pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;make this pattern extremely practical now.&lt;/p&gt;

&lt;p&gt;The operational system continues running normally while analytical systems receive data almost in real time.&lt;/p&gt;




&lt;h1&gt;
  
  
  They Even Think Differently Internally
&lt;/h1&gt;

&lt;p&gt;The differences go deeper than just "transactions vs analytics".&lt;/p&gt;

&lt;p&gt;PostgreSQL thinks heavily about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rows&lt;/li&gt;
&lt;li&gt;transactional consistency&lt;/li&gt;
&lt;li&gt;updates&lt;/li&gt;
&lt;li&gt;locking&lt;/li&gt;
&lt;li&gt;relational integrity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse thinks heavily about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;columns&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;merges&lt;/li&gt;
&lt;li&gt;partitions&lt;/li&gt;
&lt;li&gt;analytical scans&lt;/li&gt;
&lt;li&gt;aggregation efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even their storage engines reflect completely different priorities.&lt;/p&gt;




&lt;h1&gt;
  
  
  This Is Why Modern Data Stacks Often Use Both
&lt;/h1&gt;

&lt;p&gt;Once you stop viewing databases as competitors and instead view them as workload-specific systems, the architecture starts making much more sense.&lt;/p&gt;

&lt;p&gt;PostgreSQL handles the operational side.&lt;/p&gt;

&lt;p&gt;ClickHouse handles the analytical side.&lt;/p&gt;

&lt;p&gt;Together, they create systems that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;process transactions reliably&lt;/li&gt;
&lt;li&gt;scale analytical workloads efficiently&lt;/li&gt;
&lt;li&gt;support observability&lt;/li&gt;
&lt;li&gt;power dashboards&lt;/li&gt;
&lt;li&gt;retain huge historical datasets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without forcing a single database to do everything.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;The more I learn about databases, the more I realize that most modern architectures are really about separation of responsibilities.&lt;/p&gt;

&lt;p&gt;PostgreSQL and ClickHouse work well together because they optimize for fundamentally different problems.&lt;/p&gt;

&lt;p&gt;One is built to preserve business state reliably.&lt;/p&gt;

&lt;p&gt;The other is built to analyze massive amounts of history efficiently.&lt;/p&gt;

&lt;p&gt;And when combined properly, they complement each other extremely well.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>postgres</category>
      <category>clickhouse</category>
      <category>systemarchitecture</category>
    </item>
    <item>
      <title>PostgreSQL Restore Failures: It Wasn’t pgBackRest, It Was My Recovery Logic</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Wed, 06 May 2026 12:24:28 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/postgresql-restore-failures-it-wasnt-pgbackrest-it-was-my-recovery-logic-527d</link>
      <guid>https://forem.com/mohhddhassan/postgresql-restore-failures-it-wasnt-pgbackrest-it-was-my-recovery-logic-527d</guid>
      <description>&lt;p&gt;I was building and testing a PostgreSQL backup and restore workflow using pgBackRest.&lt;/p&gt;

&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;take backups&lt;/li&gt;
&lt;li&gt;restore them automatically&lt;/li&gt;
&lt;li&gt;validate the database&lt;/li&gt;
&lt;li&gt;make recovery predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, I ended up repeatedly breaking PostgreSQL recovery itself.&lt;/p&gt;

&lt;p&gt;At one point, PostgreSQL refused to start entirely, the application depending on it failed to start, and I started seeing errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invalid checkpoint record
could not locate a valid checkpoint record at 0/DEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, I also hit timeline mismatch errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: [058]: target timeline 3 forked from backup timeline 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;pgBackRest restores were corrupting PostgreSQL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption turned out to be completely wrong.&lt;/p&gt;

&lt;p&gt;The real problem was the way I was handling recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Building
&lt;/h2&gt;

&lt;p&gt;I was testing a PostgreSQL backup/restore flow locally after repeated restore failures elsewhere.&lt;/p&gt;

&lt;p&gt;To isolate the issue properly, I moved PostgreSQL onto my local machine and started testing the restore logic independently through API-triggered workflows.&lt;/p&gt;

&lt;p&gt;The restore flow looked roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download backup repo&lt;/li&gt;
&lt;li&gt;Stop PostgreSQL&lt;/li&gt;
&lt;li&gt;Restore backup&lt;/li&gt;
&lt;li&gt;Start PostgreSQL&lt;/li&gt;
&lt;li&gt;Validate database&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sounds straightforward.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The First Major Failure
&lt;/h2&gt;

&lt;p&gt;After a restore attempt, PostgreSQL refused to start.&lt;/p&gt;

&lt;p&gt;The logs looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOG: database system was interrupted
LOG: invalid checkpoint record
PANIC: could not locate a valid checkpoint record at 0/DEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL was down&lt;/li&gt;
&lt;li&gt;the application couldn't start&lt;/li&gt;
&lt;li&gt;authentication-related functionality stopped working&lt;/li&gt;
&lt;li&gt;and repeated restore attempts made things even worse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What confused me initially was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The restore itself appeared to complete.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But PostgreSQL would immediately enter recovery problems afterward.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Wrong Assumption
&lt;/h2&gt;

&lt;p&gt;This was the real issue.&lt;/p&gt;

&lt;p&gt;Every time recovery failed, I kept seeing files like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;backup_label&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recovery.signal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;standby.signal&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I assumed they were leftover artifacts from failed restores.&lt;/p&gt;

&lt;p&gt;My restore automation started aggressively cleaning them up.&lt;/p&gt;

&lt;p&gt;Something like 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="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; recovery.signal standby.signal backup_label
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I genuinely believed this was helping PostgreSQL start cleanly.&lt;/p&gt;

&lt;p&gt;In reality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I was deleting the exact recovery metadata PostgreSQL needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That misunderstanding caused almost every major issue afterward.&lt;/p&gt;




&lt;h2&gt;
  
  
  What PostgreSQL Was Actually Trying To Do
&lt;/h2&gt;

&lt;p&gt;This was the turning point.&lt;/p&gt;

&lt;p&gt;pgBackRest wasn't randomly writing junk files into the data directory.&lt;/p&gt;

&lt;p&gt;Those files exist for a reason.&lt;/p&gt;

&lt;p&gt;During restore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;backup_label&lt;/code&gt; tells PostgreSQL where recovery should begin&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recovery.signal&lt;/code&gt; tells PostgreSQL to enter recovery mode&lt;/li&gt;
&lt;li&gt;WAL replay reconstructs a consistent database state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL was actually trying to perform a valid recovery process.&lt;/p&gt;

&lt;p&gt;My automation kept interrupting or invalidating it.&lt;/p&gt;

&lt;p&gt;Once I understood that, the entire problem started making sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Recovery Loop Problem
&lt;/h2&gt;

&lt;p&gt;Because my cleanup logic removed recovery metadata prematurely, PostgreSQL ended up in inconsistent states repeatedly.&lt;/p&gt;

&lt;p&gt;Sometimes it would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enter recovery mode&lt;/li&gt;
&lt;li&gt;fail WAL replay&lt;/li&gt;
&lt;li&gt;lose checkpoint continuity&lt;/li&gt;
&lt;li&gt;refuse startup entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other times it would partially start, but remain stuck in recovery mode.&lt;/p&gt;

&lt;p&gt;That led to additional logic being added just to stabilize startup behavior.&lt;/p&gt;

&lt;p&gt;For example:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_is_in_recovery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and when required:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_promote&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal wasn't to "force PostgreSQL to work".&lt;/p&gt;

&lt;p&gt;The goal was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;let PostgreSQL finish recovery properly, then promote only when necessary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction mattered a lot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Timeline Mismatch Error
&lt;/h2&gt;

&lt;p&gt;At one stage, I also hit this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: [058]: target timeline 3 forked from backup timeline 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one was especially confusing at first.&lt;/p&gt;

&lt;p&gt;The issue was not just corrupted startup state anymore.&lt;/p&gt;

&lt;p&gt;Now PostgreSQL was rejecting WAL history itself.&lt;/p&gt;

&lt;p&gt;This happened because earlier restore attempts had already created inconsistent recovery timelines.&lt;/p&gt;

&lt;p&gt;I had essentially created multiple broken recovery histories while repeatedly testing and modifying the restore process.&lt;/p&gt;

&lt;p&gt;That was another important lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL backups are not just data files.&lt;br&gt;
They are tightly connected to WAL history and recovery timelines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point, I realized I was no longer debugging a simple restore failure. I was debugging recovery history itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Problem In My Restore Flow
&lt;/h2&gt;

&lt;p&gt;Initially, my restore logic tried to "fix" PostgreSQL after restore.&lt;/p&gt;

&lt;p&gt;That approach was fundamentally flawed.&lt;/p&gt;

&lt;p&gt;The older flow looked roughly like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old Approach&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delta restore&lt;/td&gt;
&lt;td&gt;Mixed old/new recovery state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete &lt;code&gt;backup_label&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Broke recovery metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete &lt;code&gt;recovery.signal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Interrupted recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force archive changes&lt;/td&gt;
&lt;td&gt;Caused WAL continuity issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hope PostgreSQL starts&lt;/td&gt;
&lt;td&gt;No validation or recovery awareness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I was treating recovery artifacts like corruption.&lt;/p&gt;

&lt;p&gt;They weren't corruption.&lt;/p&gt;

&lt;p&gt;They were part of PostgreSQL recovery itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Change That Finally Fixed It
&lt;/h2&gt;

&lt;p&gt;The biggest realization was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stop fighting PostgreSQL recovery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of trying to manually "clean up" PostgreSQL after restore, I changed the restore flow completely.&lt;/p&gt;

&lt;p&gt;The corrected restore flow became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop PostgreSQL cleanly&lt;/li&gt;
&lt;li&gt;Completely empty the data directory&lt;/li&gt;
&lt;li&gt;Run pgBackRest restore properly&lt;/li&gt;
&lt;li&gt;Let PostgreSQL recover normally&lt;/li&gt;
&lt;li&gt;Wait for readiness&lt;/li&gt;
&lt;li&gt;Promote only if recovery mode persists&lt;/li&gt;
&lt;li&gt;Validate using pgBackRest check&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical change was this:&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_run_pgbackrest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restore&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;--type=immediate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And equally important:&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_empty_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_data_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of attempting partial or delta-style recovery cleanup, the restore process now starts from a completely clean data directory.&lt;/p&gt;

&lt;p&gt;That eliminated a huge amount of inconsistent state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;--type=immediate&lt;/code&gt; Helped
&lt;/h2&gt;

&lt;p&gt;This turned out to be extremely important.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--type=immediate&lt;/code&gt; tells pgBackRest:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;restore to the latest immediately consistent point available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL could perform proper WAL-based recovery&lt;/li&gt;
&lt;li&gt;recovery metadata stayed intact&lt;/li&gt;
&lt;li&gt;WAL replay remained valid&lt;/li&gt;
&lt;li&gt;timeline handling became predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL itself was finally allowed to control recovery correctly.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Mistake That Increased the Blast Radius
&lt;/h2&gt;

&lt;p&gt;One thing I learned the hard way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never test restore automation against a database actively used by an application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even though this was a testing workflow, the PostgreSQL instance was still tied to application startup behavior.&lt;/p&gt;

&lt;p&gt;So whenever PostgreSQL failed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application startup failed too&lt;/li&gt;
&lt;li&gt;user-related functionality broke&lt;/li&gt;
&lt;li&gt;debugging became much harder under pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After repeated failures, I moved the restore testing flow entirely onto my local machine and isolated PostgreSQL from the rest of the application stack.&lt;/p&gt;

&lt;p&gt;That made debugging significantly easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Another Subtle Issue: Backup Failures After Restore
&lt;/h2&gt;

&lt;p&gt;I also ran into another confusing problem after some restore attempts.&lt;/p&gt;

&lt;p&gt;In certain cases, subsequent backups started failing unexpectedly after a restore.&lt;/p&gt;

&lt;p&gt;Part of the issue came from mixing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restore operations&lt;/li&gt;
&lt;li&gt;delta-style restore assumptions&lt;/li&gt;
&lt;li&gt;and archive/WAL state inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At one stage, I was also toggling archive-related behavior incorrectly during recovery experiments, which further complicated WAL continuity.&lt;/p&gt;

&lt;p&gt;This reinforced another important realization:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL backups are tightly coupled with WAL history and recovery timelines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even when the database appears to start correctly, inconsistent recovery state can break future backup behavior in subtle ways.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned From This
&lt;/h2&gt;

&lt;p&gt;This experience completely changed how I think about PostgreSQL recovery.&lt;/p&gt;

&lt;p&gt;Some major lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;backup_label&lt;/code&gt; and &lt;code&gt;recovery.signal&lt;/code&gt; are not garbage files&lt;/li&gt;
&lt;li&gt;PostgreSQL recovery is heavily WAL-dependent&lt;/li&gt;
&lt;li&gt;Timelines matter more than most people realize&lt;/li&gt;
&lt;li&gt;Partial cleanup creates inconsistent recovery states&lt;/li&gt;
&lt;li&gt;A clean restore is often safer than trying to "repair" recovery manually&lt;/li&gt;
&lt;li&gt;pgBackRest already knows how to orchestrate PostgreSQL recovery properly&lt;/li&gt;
&lt;li&gt;Restore validation matters as much as backup creation&lt;/li&gt;
&lt;li&gt;Backup testing should happen in isolated environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL recovery is not something you should "fight".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I stopped trying to override recovery behavior manually and instead allowed PostgreSQL + pgBackRest to handle recovery the way they were designed to, the restore flow finally became stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Restore Flow That Actually Worked
&lt;/h2&gt;

&lt;p&gt;After multiple failed recovery attempts, timeline mismatches, and broken startup states, I stopped trying to manually "fix" PostgreSQL recovery and instead simplified the restore process completely.&lt;/p&gt;

&lt;p&gt;The final stable flow looked roughly like this:&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="c1"&gt;# simplified restore flow
&lt;/span&gt;
&lt;span class="nf"&gt;stop_postgres&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;empty_data_directory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;pgbackrest_restore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--type=immediate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;start_postgres&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;wait_for_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;postgres_is_in_recovery&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;promote_postgres&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;pgbackrest_check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part here is not the code itself.&lt;/p&gt;

&lt;p&gt;It's the recovery philosophy behind it.&lt;/p&gt;

&lt;p&gt;The earlier versions of my restore logic tried to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;partially clean recovery state&lt;/li&gt;
&lt;li&gt;remove recovery metadata&lt;/li&gt;
&lt;li&gt;force PostgreSQL out of recovery&lt;/li&gt;
&lt;li&gt;preserve old data directory state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That approach kept creating inconsistent recovery conditions.&lt;/p&gt;

&lt;p&gt;The corrected flow instead does three important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;starts from a completely clean data directory&lt;/li&gt;
&lt;li&gt;lets pgBackRest manage recovery metadata properly&lt;/li&gt;
&lt;li&gt;allows PostgreSQL to perform WAL recovery the way it was designed to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest change was no longer treating files like &lt;code&gt;backup_label&lt;/code&gt; or &lt;code&gt;recovery.signal&lt;/code&gt; as corruption artifacts.&lt;/p&gt;

&lt;p&gt;They were part of the recovery process itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;At the beginning, I thought PostgreSQL restores were failing because the database was corrupted.&lt;/p&gt;

&lt;p&gt;In reality, the corruption was coming from my own recovery assumptions.&lt;/p&gt;

&lt;p&gt;The system wasn't broken.&lt;/p&gt;

&lt;p&gt;My mental model of PostgreSQL recovery was.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>arrayJoin in ClickHouse: Why Your Rows Are Duplicating (and How to Control It)</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Tue, 28 Apr 2026 10:20:11 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/arrayjoin-in-clickhouse-why-your-rows-are-duplicating-and-how-to-control-it-5862</link>
      <guid>https://forem.com/mohhddhassan/arrayjoin-in-clickhouse-why-your-rows-are-duplicating-and-how-to-control-it-5862</guid>
      <description>&lt;p&gt;When working with arrays in ClickHouse, &lt;code&gt;arrayJoin&lt;/code&gt; feels straightforward.&lt;/p&gt;

&lt;p&gt;Until your query suddenly returns far more rows than expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Use Case
&lt;/h2&gt;

&lt;p&gt;Let’s say you have a table like this:&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;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;actions&lt;/span&gt; &lt;span class="n"&gt;Array&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="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="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;Example row:&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;user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;click'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scroll'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;purchase'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you want each action as a separate row.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tool: &lt;code&gt;arrayJoin&lt;/code&gt;
&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arrayJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&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;action&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;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1   click
1   scroll
1   purchase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, everything looks correct.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Go Wrong
&lt;/h2&gt;

&lt;p&gt;Now let’s say you write:&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;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;arrayJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;arrayJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&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;action2&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;You might expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you actually get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;9 rows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arrayJoin&lt;/code&gt; doesn’t just flatten arrays.&lt;/p&gt;

&lt;p&gt;It expands rows.&lt;/p&gt;

&lt;p&gt;Each element in the array creates a new row.&lt;/p&gt;

&lt;p&gt;So when you use it multiple times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First &lt;code&gt;arrayJoin&lt;/code&gt; → expands rows&lt;/li&gt;
&lt;li&gt;Second &lt;code&gt;arrayJoin&lt;/code&gt; → expands again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;3 elements → 3 × 3 = 9 rows&lt;/p&gt;

&lt;p&gt;This is effectively a cartesian multiplication of rows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Impact
&lt;/h2&gt;

&lt;p&gt;This becomes a real problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arrays are large&lt;/li&gt;
&lt;li&gt;Multiple &lt;code&gt;arrayJoin&lt;/code&gt;s are used&lt;/li&gt;
&lt;li&gt;You don’t expect row multiplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect output&lt;/li&gt;
&lt;li&gt;Sudden increase in row count&lt;/li&gt;
&lt;li&gt;Slower queries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Better Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Use a single &lt;code&gt;arrayJoin&lt;/code&gt; when possible
&lt;/h3&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;arrayJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&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;action&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;h3&gt;
  
  
  2. Use &lt;code&gt;ARRAY JOIN&lt;/code&gt; syntax (cleaner and explicit)
&lt;/h3&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Use &lt;code&gt;arrayZip&lt;/code&gt; to avoid unintended multiplication
&lt;/h3&gt;

&lt;p&gt;If you’re working with multiple arrays:&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;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;arrayJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arrayZip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actions&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;zipped&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;This ensures elements are paired instead of multiplied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arrayJoin&lt;/code&gt; is powerful-but easy to misuse.&lt;/p&gt;

&lt;p&gt;If used without understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row count can explode&lt;/li&gt;
&lt;li&gt;Queries become expensive&lt;/li&gt;
&lt;li&gt;Results can be misleading&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Event tracking pipelines&lt;/li&gt;
&lt;li&gt;Flattening nested JSON&lt;/li&gt;
&lt;li&gt;Working with semi-structured logs&lt;/li&gt;
&lt;li&gt;Exploding arrays into rows for analysis&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One Important Gotcha
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;arrayJoin&lt;/code&gt; multiplies rows.&lt;/p&gt;

&lt;p&gt;If your result size looks unexpectedly large, this is one of the first things to check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arrayJoin&lt;/code&gt; is one of the most useful tools in ClickHouse.&lt;/p&gt;

&lt;p&gt;But its behavior is not always intuitive.&lt;/p&gt;

&lt;p&gt;In many cases, the issue is not the data itself-but how the query expands it.&lt;/p&gt;

&lt;p&gt;Understanding this early can save a lot of debugging time.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>greatCircleDistance in ClickHouse: Avoiding Full Table Scans</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Mon, 20 Apr 2026 16:18:23 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/greatcircledistance-in-clickhouse-avoiding-full-table-scans-375p</link>
      <guid>https://forem.com/mohhddhassan/greatcircledistance-in-clickhouse-avoiding-full-table-scans-375p</guid>
      <description>&lt;p&gt;When working with location data, one problem shows up almost immediately:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do I calculate the distance between two coordinates stored in my database?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, it seems like something you’d have to handle outside the database.&lt;/p&gt;

&lt;p&gt;But if you're using ClickHouse, there’s a built-in function for this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Right Tool: &lt;code&gt;greatCircleDistance&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;greatCircleDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It calculates the shortest distance between two points on Earth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&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="n"&gt;greatCircleDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0827&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2707&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9716&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5946&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;distance_meters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the distance between Chennai and Bangalore - in &lt;strong&gt;meters&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Looks Simple… But There’s a Catch
&lt;/h2&gt;

&lt;p&gt;Now let’s say you write a query like this:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;greatCircleDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0827&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2707&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks perfectly fine.&lt;/p&gt;

&lt;p&gt;But this can quietly turn into a full table scan - especially on large datasets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;In ClickHouse, indexes don’t work like traditional B-tree indexes.&lt;/p&gt;

&lt;p&gt;They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sparse&lt;/li&gt;
&lt;li&gt;Designed for &lt;strong&gt;range pruning&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They work well for queries like:&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But not for:&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;greatCircleDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The function is applied on the columns, so ClickHouse cannot use the index to skip data efficiently.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Better Approach (What You Should Actually Do)
&lt;/h2&gt;

&lt;p&gt;Instead of directly applying the function, &lt;strong&gt;reduce the dataset first&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bounding Box Filter
&lt;/h3&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="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0827&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;05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0827&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;05&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;lon&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2707&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;05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2707&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;05&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;greatCircleDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0827&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2707&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;(The bounding box is an approximation to reduce the search space before exact filtering.)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lat BETWEEN&lt;/code&gt; → uses index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lon BETWEEN&lt;/code&gt; → reduces rows further&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;greatCircleDistance&lt;/code&gt; → applied only on filtered data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of scanning the entire table:&lt;br&gt;
 =&amp;gt; You narrow it down first, then compute accurately&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;p&gt;This pattern is useful in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivery radius filtering&lt;/li&gt;
&lt;li&gt;Finding nearby users&lt;/li&gt;
&lt;li&gt;Geo-based analytics&lt;/li&gt;
&lt;li&gt;Ride-sharing systems&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One Important Gotcha
&lt;/h2&gt;

&lt;p&gt;Make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coordinates are in &lt;strong&gt;degrees (not radians)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Order is always &lt;code&gt;(lat, lon)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swapping them will give incorrect results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;greatCircleDistance&lt;/code&gt; is powerful - but if used blindly, it can hurt performance.&lt;/p&gt;

&lt;p&gt;In ClickHouse, performance often depends more on how you query than what you query.&lt;/p&gt;

&lt;p&gt;Sometimes, the right approach isn’t just using a function - but knowing &lt;strong&gt;when and how to use it efficiently&lt;/strong&gt;.&lt;/p&gt;




</description>
      <category>clickhouse</category>
      <category>sql</category>
      <category>dataengineering</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why My S3 Backup Setup Broke: Buckets, “Folders”, and Scheduling Misconceptions</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Thu, 16 Apr 2026 07:04:20 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/why-my-s3-backup-setup-broke-buckets-folders-and-scheduling-misconceptions-315e</link>
      <guid>https://forem.com/mohhddhassan/why-my-s3-backup-setup-broke-buckets-folders-and-scheduling-misconceptions-315e</guid>
      <description>&lt;p&gt;&lt;em&gt;Another lesson in building reliable systems - not just configuring them.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I thought I had everything set up correctly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backups configured&lt;/li&gt;
&lt;li&gt;S3-compatible storage connected&lt;/li&gt;
&lt;li&gt;Backup triggered via cron jobs during testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yet nothing showed up where I expected.&lt;/p&gt;

&lt;p&gt;What looked like a simple configuration issue turned out to be a wrong mental model of how S3 actually works.&lt;/p&gt;

&lt;p&gt;This post breaks down what went wrong and what fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An S3-compatible object storage (not AWS directly)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A system that allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuring a bucket&lt;/li&gt;
&lt;li&gt;Setting a backup path&lt;/li&gt;
&lt;li&gt;Defining backup frequency&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Everything seemed straightforward.&lt;/p&gt;

&lt;p&gt;But the problem started with one assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Buckets can behave like folders.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The First Mistake: Treating Buckets Like Folders
&lt;/h2&gt;

&lt;p&gt;In a traditional file system, you think like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backups/
  app1/
    db.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it felt natural to assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a “folder” in object storage&lt;/li&gt;
&lt;li&gt;Then create buckets inside it for different use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8owl9w7awnhyhx94611.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8owl9w7awnhyhx94611.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case, I had something like a folder already created in the object storage UI, and I assumed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That is my base, and I can create buckets under it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connecting to that “folder” as a bucket&lt;/li&gt;
&lt;li&gt;Then creating another bucket inside it (for vector DB backups)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kept failing.&lt;/p&gt;

&lt;p&gt;At first, I thought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maybe it is a permission issue&lt;/li&gt;
&lt;li&gt;Maybe my user does not have enough access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that was not the real problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Actually Going Wrong
&lt;/h2&gt;

&lt;p&gt;I was effectively trying to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat a bucket like a parent directory&lt;/li&gt;
&lt;li&gt;And create another bucket inside it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not how S3 works.&lt;/p&gt;

&lt;p&gt;In S3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buckets are top-level containers&lt;/li&gt;
&lt;li&gt;You cannot nest buckets inside other buckets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when I tried to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to an existing bucket&lt;/li&gt;
&lt;li&gt;And then create another bucket under it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It failed because the concept itself is invalid.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Correct Mental Model
&lt;/h2&gt;

&lt;p&gt;This is how S3 actually works:&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;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backups&lt;/span&gt;
&lt;span class="na"&gt;object key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app1/2026-04-15/db.sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are only two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bucket (top-level)&lt;/li&gt;
&lt;li&gt;Object key (full path as a string)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no real folder hierarchy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Organizing Data the Right Way
&lt;/h2&gt;

&lt;p&gt;The fix was not about creating folders.&lt;/p&gt;

&lt;p&gt;It was about changing how I name objects.&lt;/p&gt;

&lt;p&gt;Instead of trying to structure things at the bucket level, I moved that structure into the object key.&lt;/p&gt;

&lt;p&gt;For example:&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="n"&gt;object_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snapshot_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives a structure like:&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;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backups&lt;/span&gt;

&lt;span class="s"&gt;qdrant/&lt;/span&gt;
  &lt;span class="s"&gt;collection_1/&lt;/span&gt;
    &lt;span class="s"&gt;snapshot_001&lt;/span&gt;
  &lt;span class="s"&gt;collection_2/&lt;/span&gt;
    &lt;span class="s"&gt;snapshot_002&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though S3 is flat internally, most UIs render this as a folder-like structure.&lt;/p&gt;

&lt;p&gt;This is the correct way to organize data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Mistake: Mixing Bucket and Path
&lt;/h2&gt;

&lt;p&gt;Another issue was passing paths as part of the bucket name.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;backups/qdrant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is invalid.&lt;/p&gt;

&lt;p&gt;Correct approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bucket = backups&lt;/li&gt;
&lt;li&gt;object key = qdrant/collection_name/snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 APIs expect a valid bucket name, not a path.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Finally Clicked
&lt;/h2&gt;

&lt;p&gt;The breakthrough was realizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I was not dealing with folders at all&lt;/li&gt;
&lt;li&gt;I was dealing with string prefixes inside object keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I stopped trying to create hierarchy at the bucket level and moved everything into object naming, the entire setup started working as expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Correct configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bucket:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  backups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Object naming:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snapshot_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone was enough to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organize backups cleanly&lt;/li&gt;
&lt;li&gt;Avoid bucket-related errors&lt;/li&gt;
&lt;li&gt;Make the storage layout intuitive in the UI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Buckets are not folders&lt;/li&gt;
&lt;li&gt;You cannot create a bucket inside another bucket&lt;/li&gt;
&lt;li&gt;S3 is a flat object store&lt;/li&gt;
&lt;li&gt;Folder-like structures come from object key prefixes&lt;/li&gt;
&lt;li&gt;Always keep bucket and path separate&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The issue was not with permissions or configuration.&lt;/p&gt;

&lt;p&gt;It was a mismatch between how I expected storage to behave and how it actually works.&lt;/p&gt;

&lt;p&gt;Once the mental model changed, the implementation became simple.&lt;/p&gt;

&lt;p&gt;If something feels unnecessarily complicated in S3, it is often a sign that the model being used is incorrect.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloudstorage</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Debugging a Broken Metrics Pipeline: What Actually Went Wrong</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Thu, 16 Apr 2026 03:29:50 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/debugging-a-broken-metrics-pipeline-what-actually-went-wrong-4d3o</link>
      <guid>https://forem.com/mohhddhassan/debugging-a-broken-metrics-pipeline-what-actually-went-wrong-4d3o</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of a series on building a metrics pipeline into ClickHouse&lt;br&gt;
Read &lt;a href="https://dev.to/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k"&gt;Part 3: Understanding Vector Transforms&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  When Things Still Don’t Work
&lt;/h2&gt;

&lt;p&gt;At this point, the pipeline looked correct.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sources were defined&lt;/li&gt;
&lt;li&gt;Transforms were working&lt;/li&gt;
&lt;li&gt;Data structure matched expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yet, something was still off.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data wasn’t behaving the way it should.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where debugging became the main task.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Only Way Forward: Logs
&lt;/h2&gt;

&lt;p&gt;When dealing with ingestion issues in ClickHouse, logs become your best source of truth.&lt;/p&gt;

&lt;p&gt;I started monitoring the error logs directly:&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 tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/clickhouse-server/clickhouse-server.err.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately surfaced issues that were not visible from the pipeline configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  An Error That Didn’t Make Sense
&lt;/h2&gt;

&lt;p&gt;At one point, I started seeing this error repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There exists no table monitoring.cpu in database monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was confusing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I hadn’t created a table named &lt;code&gt;cpu&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It wasn’t part of my current setup&lt;/li&gt;
&lt;li&gt;My Vector configuration didn’t reference it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So where was it coming from?&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Actually Happening
&lt;/h2&gt;

&lt;p&gt;After digging deeper, the issue had nothing to do with my current pipeline.&lt;/p&gt;

&lt;p&gt;It turned out that a previously used Telegraf process was still running in the background.&lt;/p&gt;

&lt;p&gt;Even though I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed configurations&lt;/li&gt;
&lt;li&gt;Switched tools&lt;/li&gt;
&lt;li&gt;Rebuilt the pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old process was still active and sending data using an outdated setup.&lt;/p&gt;

&lt;p&gt;That’s why ClickHouse was reporting errors for a table I never intended to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Problem
&lt;/h2&gt;

&lt;p&gt;This wasn’t a configuration issue.&lt;/p&gt;

&lt;p&gt;It was a &lt;strong&gt;runtime issue&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The system I was debugging was not the only system running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That realization changed how I approached debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fixing It
&lt;/h2&gt;

&lt;p&gt;The solution was simple - but easy to miss.&lt;/p&gt;

&lt;p&gt;First, I checked for any running Telegraf processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;telegraf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then stopped them explicitly:&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;systemctl stop telegraf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the old process was stopped, the errors disappeared.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Teaches
&lt;/h2&gt;

&lt;p&gt;This led to an important lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always validate the runtime environment - not just the configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When working with pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old processes may still be running&lt;/li&gt;
&lt;li&gt;Multiple agents may write to the same destination&lt;/li&gt;
&lt;li&gt;Previous setups can interfere with new ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t account for this, you may end up debugging the wrong problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Debugging Loop
&lt;/h2&gt;

&lt;p&gt;Most of the pipeline development ended up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write → Run → Fail → Check logs → Fix → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each iteration helped refine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transform logic&lt;/li&gt;
&lt;li&gt;Data structure&lt;/li&gt;
&lt;li&gt;Schema alignment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This loop is where real progress happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Finally Worked
&lt;/h2&gt;

&lt;p&gt;Once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transforms were correct&lt;/li&gt;
&lt;li&gt;Timestamps were fixed&lt;/li&gt;
&lt;li&gt;Old processes were stopped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline stabilized.&lt;/p&gt;

&lt;p&gt;Data started flowing consistently into ClickHouse, and queries returned expected results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series Recap
&lt;/h2&gt;

&lt;p&gt;This series covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h"&gt;Part 1: Why the Telegraf approach didn’t work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k"&gt;Part 2: Understanding Vector pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/from-pipelines-to-transforms-making-vector-work-with-clickhouse-5ho"&gt;Part 3: Writing transforms and handling data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 4: Debugging and making the pipeline reliable (this post)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Building data pipelines is rarely about getting things right on the first try.&lt;/p&gt;

&lt;p&gt;It’s about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observing how the system behaves&lt;/li&gt;
&lt;li&gt;Identifying where it breaks&lt;/li&gt;
&lt;li&gt;Iterating until it stabilizes&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Debugging is not a side task - it is the process.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dataengineering</category>
      <category>clickhouse</category>
      <category>vector</category>
      <category>debugging</category>
    </item>
    <item>
      <title>From Pipelines to Transforms: Making Vector Work with ClickHouse</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Thu, 16 Apr 2026 01:13:59 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/from-pipelines-to-transforms-making-vector-work-with-clickhouse-5ho</link>
      <guid>https://forem.com/mohhddhassan/from-pipelines-to-transforms-making-vector-work-with-clickhouse-5ho</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of a series on building a metrics pipeline into ClickHouse&lt;br&gt;
Read &lt;a href="https://dev.to/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k"&gt;Part 2: Understanding Vector Pipelines&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Where Things Got Real
&lt;/h2&gt;

&lt;p&gt;By this point, the pipeline structure made sense.&lt;/p&gt;

&lt;p&gt;I understood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sources&lt;/li&gt;
&lt;li&gt;Transforms&lt;/li&gt;
&lt;li&gt;Sinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the pipeline still wasn’t working reliably.&lt;/p&gt;

&lt;p&gt;That’s when it became clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The hardest part wasn’t collecting data.&lt;br&gt;
It was transforming it correctly.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Why Transforms Matter
&lt;/h2&gt;

&lt;p&gt;Raw metrics are rarely usable as-is.&lt;/p&gt;

&lt;p&gt;When sending data into ClickHouse, even small inconsistencies can break ingestion.&lt;/p&gt;

&lt;p&gt;Some common issues encountered were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong data types&lt;/li&gt;
&lt;li&gt;Unexpected field structures&lt;/li&gt;
&lt;li&gt;Missing values&lt;/li&gt;
&lt;li&gt;Incorrect timestamp formats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if everything else is correct, these issues cause inserts to fail.&lt;/p&gt;


&lt;h2&gt;
  
  
  Enter VRL
&lt;/h2&gt;

&lt;p&gt;In Vector, transformations are written using &lt;strong&gt;Vector Remap Language&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first, VRL feels simple.&lt;/p&gt;

&lt;p&gt;But in practice, it’s strict.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Types must be explicit&lt;/li&gt;
&lt;li&gt;Fields must be handled carefully&lt;/li&gt;
&lt;li&gt;Errors are not ignored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That strictness is what makes pipelines reliable - but also harder to get right.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Timestamp Problem
&lt;/h2&gt;

&lt;p&gt;One of the biggest issues I faced was timestamp handling.&lt;/p&gt;

&lt;p&gt;ClickHouse expects timestamps in a specific format.&lt;/p&gt;

&lt;p&gt;The raw data didn’t match that format.&lt;/p&gt;

&lt;p&gt;Even when everything else was correct, inserts would fail silently because of this.&lt;/p&gt;

&lt;p&gt;The fix looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.timestamp = to_unix_timestamp!(parse_timestamp!(.timestamp, "%+"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line did three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsed the incoming timestamp&lt;/li&gt;
&lt;li&gt;Converted it into a Unix format&lt;/li&gt;
&lt;li&gt;Made it compatible with ClickHouse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It seems simple - but this was a major blocker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Normalizing Metrics
&lt;/h2&gt;

&lt;p&gt;Another challenge was aligning the data structure with what ClickHouse expects.&lt;/p&gt;

&lt;p&gt;For both host and GPU metrics, this required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting values into numeric types&lt;/li&gt;
&lt;li&gt;Standardizing field names&lt;/li&gt;
&lt;li&gt;Adding metadata like host and source&lt;/li&gt;
&lt;li&gt;Ensuring consistent structure across all metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this step, ingestion would fail even if the pipeline looked correct.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Raw Data to Queryable Format
&lt;/h2&gt;

&lt;p&gt;One important transformation was changing how metrics were structured.&lt;/p&gt;

&lt;p&gt;Instead of storing multiple values in a single record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cpu, memory, disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data was reshaped into a row-based format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metric_name = "cpu", value = ...
metric_name = "memory", value = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made it easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query data in ClickHouse&lt;/li&gt;
&lt;li&gt;Aggregate metrics&lt;/li&gt;
&lt;li&gt;Maintain a consistent schema&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Was Hard
&lt;/h2&gt;

&lt;p&gt;Most of the time spent on this pipeline wasn’t on setup.&lt;/p&gt;

&lt;p&gt;It was here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write transform → Run → Fail → Fix → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each iteration revealed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A type mismatch&lt;/li&gt;
&lt;li&gt;A missing field&lt;/li&gt;
&lt;li&gt;A formatting issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the pipeline actually gets built.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed After This
&lt;/h2&gt;

&lt;p&gt;Once the transforms were correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data started flowing reliably&lt;/li&gt;
&lt;li&gt;Inserts into ClickHouse succeeded&lt;/li&gt;
&lt;li&gt;Queries started returning meaningful results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the pipeline finally felt stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;Even after fixing transformations, one major challenge remained:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Debugging unexpected failures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the next part, I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How I debugged pipeline issues&lt;/li&gt;
&lt;li&gt;What ClickHouse logs revealed&lt;/li&gt;
&lt;li&gt;And a mistake that cost me time&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Series Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h"&gt;Part 1: Why the Telegraf approach didn’t work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k"&gt;Part 2: Understanding Vector Pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 3: Writing transforms and handling data correctly (this post)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/debugging-a-broken-metrics-pipeline-what-actually-went-wrong-4d3o"&gt;Part 4: Debugging and making the pipeline reliable&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Transforms are where pipelines either succeed or fail.&lt;/p&gt;

&lt;p&gt;Understanding how data needs to be shaped is more important than the tool itself.&lt;/p&gt;

&lt;p&gt;Once that becomes clear, everything else starts to fall into place.&lt;/p&gt;




</description>
      <category>dataengineering</category>
      <category>clickhouse</category>
      <category>observability</category>
      <category>vector</category>
    </item>
    <item>
      <title>Understanding Vector Pipelines: From Config Files to Data Flow</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Thu, 09 Apr 2026 19:29:55 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k</link>
      <guid>https://forem.com/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of a series on building a metrics pipeline into ClickHouse&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev.to/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h"&gt;Read Part 1: Why my metrics pipeline with Telegraf didn’t work&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Picking Up Where Things Broke
&lt;/h2&gt;

&lt;p&gt;In the previous part, I talked about trying to build a metrics pipeline using Telegraf - and why that approach didn’t work for my use case.&lt;/p&gt;

&lt;p&gt;The biggest issue wasn’t just tooling.&lt;/p&gt;

&lt;p&gt;It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I didn’t have enough control over how data moved through the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s what led me to explore a different approach.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Vector
&lt;/h2&gt;

&lt;p&gt;I came across Vector while looking for something more flexible.&lt;/p&gt;

&lt;p&gt;At a glance, it felt different.&lt;/p&gt;

&lt;p&gt;Instead of thinking in terms of plugins and configs, Vector is built around a &lt;strong&gt;pipeline model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that changes everything.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Idea: Pipelines
&lt;/h2&gt;

&lt;p&gt;At the center of Vector is a simple concept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sources → Transforms → Sinks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;But this model makes the flow of data explicit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sources&lt;/strong&gt; → where data comes from&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transforms&lt;/strong&gt; → how data is modified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sinks&lt;/strong&gt; → where data is sent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compared to my earlier approach, this immediately felt clearer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Actually Means
&lt;/h2&gt;

&lt;p&gt;Instead of writing a config and hoping everything connects correctly, you define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data you are collecting&lt;/li&gt;
&lt;li&gt;How that data should be shaped&lt;/li&gt;
&lt;li&gt;Where that data should go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That shift sounds small - but it changes how you think about the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Config Files to Data Flow
&lt;/h2&gt;

&lt;p&gt;With Telegraf, my thinking looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write config → Run → Debug errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Vector, it started becoming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Collect → Transform → Route → Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The focus moved from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“What config do I write?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“How does data move through each stage?”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The New Learning Curve
&lt;/h2&gt;

&lt;p&gt;Of course, switching tools didn’t magically solve everything.&lt;/p&gt;

&lt;p&gt;There were new challenges.&lt;/p&gt;

&lt;p&gt;Vector uses YAML for configuration, which was different from the TOML I was used to.&lt;/p&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pipeline only works if every stage is defined correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some of the early issues I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect source definitions&lt;/li&gt;
&lt;li&gt;Misconfigured sinks&lt;/li&gt;
&lt;li&gt;Data not flowing as expected&lt;/li&gt;
&lt;li&gt;Silent failures when something didn’t connect properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At times, it felt like nothing was happening-even though everything looked “correct.”&lt;/p&gt;




&lt;h2&gt;
  
  
  First Realization: Everything Is Connected
&lt;/h2&gt;

&lt;p&gt;One important thing I learned quickly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If one stage breaks, the entire pipeline breaks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike simpler setups, you can’t treat components independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bad transform can stop data entirely&lt;/li&gt;
&lt;li&gt;A misconfigured sink can drop everything silently&lt;/li&gt;
&lt;li&gt;A source that doesn’t emit correctly makes debugging harder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This forced me to start thinking in terms of &lt;strong&gt;end-to-end flow&lt;/strong&gt;, not individual pieces.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Improved Immediately
&lt;/h2&gt;

&lt;p&gt;Despite the challenges, a few things became better compared to before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear visibility into how data moves&lt;/li&gt;
&lt;li&gt;Better control over transformations&lt;/li&gt;
&lt;li&gt;More flexibility in shaping data before sending it to ClickHouse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though things weren’t fully working yet, I finally felt like I was closer to solving the actual problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Still Missing
&lt;/h2&gt;

&lt;p&gt;At this stage, the pipeline structure made sense.&lt;/p&gt;

&lt;p&gt;But one part was still unclear-and turned out to be the hardest:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How to correctly transform the data so that ClickHouse would accept it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where most of the complexity showed up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;In the next part, I’ll dive into the most challenging part of this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing transforms using &lt;strong&gt;Vector Remap Language (VRL)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Handling strict data types&lt;/li&gt;
&lt;li&gt;Fixing timestamp issues&lt;/li&gt;
&lt;li&gt;And shaping metrics into a format that ClickHouse can actually ingest&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Series Overview
&lt;/h2&gt;

&lt;p&gt;This post is part of a series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h"&gt;Part 1: Why the Telegraf approach didn’t work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 2: Understanding Vector pipelines (this post)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/from-pipelines-to-transforms-making-vector-work-with-clickhouse-5ho"&gt;Part 3: Writing transforms and handling data correctly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/debugging-a-broken-metrics-pipeline-what-actually-went-wrong-4d3o"&gt;Part 4: Debugging and making the pipeline reliable &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Switching tools didn’t solve the problem immediately.&lt;/p&gt;

&lt;p&gt;But it did something more important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It made the system visible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I could see how data moved through each stage, debugging stopped being guesswork-and started becoming structured.&lt;/p&gt;




</description>
      <category>dataengineering</category>
      <category>clickhouse</category>
      <category>observability</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why My Metrics Pipeline with Telegraf Didn’t Work (and What I Learned)</title>
      <dc:creator>Mohamed Hussain S</dc:creator>
      <pubDate>Tue, 07 Apr 2026 10:18:51 +0000</pubDate>
      <link>https://forem.com/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h</link>
      <guid>https://forem.com/mohhddhassan/why-my-metrics-pipeline-with-telegraf-didnt-work-and-what-i-learned-4g6h</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of a series on building a metrics pipeline into ClickHouse&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Collecting metrics is easy.&lt;/p&gt;

&lt;p&gt;Shipping them to an analytical database without losing your mind is the hard part.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;At one point, the task seemed straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Collect system metrics (CPU, memory, GPU) and store them in ClickHouse for analysis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a common observability use case.&lt;br&gt;
You collect metrics, send them somewhere, and run queries on top.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;/p&gt;

&lt;p&gt;But in practice, it didn’t go as planned.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Initial Approach: Telegraf
&lt;/h2&gt;

&lt;p&gt;I started with Telegraf.&lt;/p&gt;

&lt;p&gt;It’s widely used for collecting system metrics and has a plugin-based architecture, which makes it a natural first choice.&lt;/p&gt;

&lt;p&gt;This was also where I first came across TOML.&lt;/p&gt;

&lt;p&gt;At first, it felt like I just needed to “write a config and run it.”&lt;br&gt;
But very quickly, I realized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Configuration isn’t just syntax-it defines how your system behaves.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What I Was Trying to Build
&lt;/h2&gt;

&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect host-level metrics (CPU, memory, etc.)&lt;/li&gt;
&lt;li&gt;Collect GPU metrics&lt;/li&gt;
&lt;li&gt;Push everything into ClickHouse&lt;/li&gt;
&lt;li&gt;Run analytical queries on top&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Essentially, a basic observability pipeline.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where Things Started Breaking
&lt;/h2&gt;

&lt;p&gt;On paper, Telegraf looked like it should work.&lt;/p&gt;

&lt;p&gt;In reality, I ran into a few issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No straightforward way to push data into ClickHouse&lt;/li&gt;
&lt;li&gt;Lack of a native ClickHouse output plugin&lt;/li&gt;
&lt;li&gt;Debugging wasn’t very intuitive&lt;/li&gt;
&lt;li&gt;Configurations became rigid as complexity increased&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point, I was spending more time trying to make the tool fit the use case than actually solving the problem.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Shift in Perspective
&lt;/h2&gt;

&lt;p&gt;This is where something important clicked.&lt;/p&gt;

&lt;p&gt;Up until this point, I was thinking in terms of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write config → Run tool → Expect output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that approach wasn’t working.&lt;/p&gt;

&lt;p&gt;What I needed instead was a clearer understanding of how data actually flows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data source → Transformation → Destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem wasn’t just the tool-it was the lack of control over how data moved through the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Decided to Move Away
&lt;/h2&gt;

&lt;p&gt;At this stage, it became clear that I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More control over data transformations&lt;/li&gt;
&lt;li&gt;Better visibility into how data flows&lt;/li&gt;
&lt;li&gt;A system that is easier to debug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Telegraf, while powerful, didn’t give me that level of flexibility for this use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;That’s when I decided to try a different approach using Vector.&lt;/p&gt;

&lt;p&gt;Instead of treating configuration as static setup, Vector treats it as a &lt;strong&gt;pipeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the next part, I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How Vector pipelines work&lt;/li&gt;
&lt;li&gt;Why the &lt;em&gt;sources → transforms → sinks&lt;/em&gt; model made a difference&lt;/li&gt;
&lt;li&gt;And what changed when I adopted that approach&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Series Overview
&lt;/h2&gt;

&lt;p&gt;This post is part of a series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: Why the Telegraf approach didn’t work (this post)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/understanding-vector-pipelines-from-config-files-to-data-flow-i8k"&gt;Part 2: Understanding Vector pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/from-pipelines-to-transforms-making-vector-work-with-clickhouse-5ho"&gt;Part 3: Writing transforms and handling data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohhddhassan/debugging-a-broken-metrics-pipeline-what-actually-went-wrong-4d3o"&gt;Part 4: Debugging and making the pipeline reliable&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;What started as a simple setup turned into a deeper lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tools don’t solve problems-understanding systems does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that became clear, the direction forward was much easier.&lt;/p&gt;




</description>
      <category>dataengineering</category>
      <category>clickhouse</category>
      <category>devops</category>
      <category>observability</category>
    </item>
  </channel>
</rss>
