<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ClickHouse</title>
    <description>The latest articles on DEV Community by ClickHouse (@clickhousedb).</description>
    <link>https://dev.to/clickhousedb</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%2F3274176%2F331ef44b-8525-44a9-9828-7bf6c5449d39.png</url>
      <title>DEV Community: ClickHouse</title>
      <link>https://dev.to/clickhousedb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clickhousedb"/>
    <language>en</language>
    <item>
      <title>Join me if you can: ClickHouse vs. Databricks &amp; Snowflake - Part 1</title>
      <dc:creator>ClickHouse</dc:creator>
      <pubDate>Wed, 02 Jul 2025 15:00:00 +0000</pubDate>
      <link>https://dev.to/clickhouse/join-me-if-you-can-clickhouse-vs-databricks-snowflake-part-1-142l</link>
      <guid>https://dev.to/clickhouse/join-me-if-you-can-clickhouse-vs-databricks-snowflake-part-1-142l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;
We took a public benchmark that tests join-heavy SQL queries on Databricks and Snowflake and ran the exact same queries on ClickHouse Cloud.&lt;br&gt;&lt;br&gt;ClickHouse was &lt;strong&gt;faster and cheaper&lt;/strong&gt; at every scale, from 721 million to 7.2 billion rows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ”ClickHouse can’t do joins.” Let’s test that
&lt;/h2&gt;

&lt;p&gt;Let’s be crystal clear upfront: &lt;strong&gt;this is not our benchmark&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Someone else designed a &lt;a href="https://github.com/JosueBogran/coffeeshopdatageneratorv2" rel="noopener noreferrer"&gt;coffee-shop-themed benchmark&lt;/a&gt; to &lt;a href="https://www.linkedin.com/pulse/databricks-vs-snowflake-gen-1-2-sql-performance-test-day-bogran-ddmhe/" rel="noopener noreferrer"&gt;compare&lt;/a&gt; both cost and performance when running join-heavy queries on Databricks and Snowflake, across different compute sizes. The benchmark author shared the full dataset and query suite publicly.&lt;/p&gt;

&lt;p&gt;Out of curiosity, we took that same benchmark, loaded the data into ClickHouse Cloud, used similar instance sizes, and ran the original 17 queries. Most queries involve joins, and we didn’t rewrite them (queries 6, 10 and 15 required minor syntax changes to work in ClickHouse SQL dialect).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We did &lt;strong&gt;no tuning at all&lt;/strong&gt;, not for the queries, and not on the ClickHouse side (no changes to table schemas, indexes, settings, etc).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next time someone says “ClickHouse can’t do joins,” &lt;strong&gt;just send them this blog&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We’ve spent the past 6 months &lt;a href="https://youtu.be/gd3OyQzB_Fc?si=Fso5oedhSHYYW16L&amp;amp;t=137" rel="noopener noreferrer"&gt;making join performance radically better in ClickHouse&lt;/a&gt;, and this post is your first look at how far we’ve come. (&lt;em&gt;Spoiler: it’s really fast. And really cheap. And we’re just getting started.&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;We’ll walk through how we ran the benchmark, how you can run it too, and then dive into the full results across three dataset sizes: 721 million, 1.4 billion, and 7.2 billion rows.&lt;/p&gt;

&lt;p&gt;Finally, we’ll wrap up with a simple takeaway: ClickHouse can do joins, and it can do it fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reproduce it
&lt;/h2&gt;

&lt;p&gt;You’ll find everything in this &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;, including all &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/blob/main/clickhouse-cloud/queries.sql" rel="noopener noreferrer"&gt;17 queries&lt;/a&gt;, scripts, and &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main/clickhouse-cloud#clickhouse-cloud-benchmark-runner" rel="noopener noreferrer"&gt;instructions&lt;/a&gt;. We’ve also published the &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main?tab=readme-ov-file#get-the-data" rel="noopener noreferrer"&gt;full datasets in a public S3 bucket&lt;/a&gt;, so you can skip the generation step and jump straight to testing.&lt;/p&gt;

&lt;p&gt;The whole thing is &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main/clickhouse-cloud#clickhouse-cloud-benchmark-runner" rel="noopener noreferrer"&gt;automated&lt;/a&gt;: spin up a ClickHouse Cloud service, set your credentials via environment variables, and run one command with your cluster specs and &lt;a href="https://clickhouse.com/pricing" rel="noopener noreferrer"&gt;price per compute unit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Click a button. Grab a coffee. And your results are ready.&lt;/p&gt;

&lt;p&gt;We run each query 5 times and report the fastest run, to reflect warm-cache performance fairly. &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main/clickhouse-cloud/results" rel="noopener noreferrer"&gt;See full results.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One dataset, many runs: how we benchmarked at scale
&lt;/h2&gt;

&lt;p&gt;To speed up the benchmarking process, we took advantage of ClickHouse Cloud &lt;a href="https://clickhouse.com/blog/introducing-warehouses-compute-compute-separation-in-clickhouse-cloud" rel="noopener noreferrer"&gt;Warehouses&lt;/a&gt;, a feature that lets you spin up multiple compute services over a single shared dataset.&lt;/p&gt;

&lt;p&gt;We ingested the data once, then spun up additional services in different sizes, &lt;strong&gt;varying the number of nodes, CPU cores, and RAM&lt;/strong&gt;, to benchmark different hardware and cost configurations.&lt;/p&gt;

&lt;p&gt;Because all services in a Warehouse share the same data, we could run the same benchmark across all configurations at once, without reloading anything.&lt;/p&gt;

&lt;p&gt;This also let us test ClickHouse Cloud’s &lt;a href="https://clickhouse.com/docs/deployment-guides/parallel-replicas" rel="noopener noreferrer"&gt;Parallel Replicas&lt;/a&gt; feature, where &lt;strong&gt;multiple compute nodes process a single query in parallel&lt;/strong&gt; for even faster results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark structure
&lt;/h2&gt;

&lt;p&gt;The original benchmark was posted in two parts on LinkedIn (&lt;a href="https://www.linkedin.com/pulse/databricks-vs-snowflake-sql-performance-test-day-1-721m-bogran-lsboe/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; and &lt;a href="https://www.linkedin.com/pulse/databricks-vs-snowflake-gen-1-2-sql-performance-test-day-bogran-ddmhe/" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;), using synthetic data that simulates orders at a national coffee chain. It tested three data scales for the main fact table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scale factor&lt;/th&gt;
&lt;th&gt;Total rows in fact table (Sales)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;500m&lt;/td&gt;
&lt;td&gt;721m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1b&lt;/td&gt;
&lt;td&gt;1.4b&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5b&lt;/td&gt;
&lt;td&gt;7.2b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three data scales use the same schema with &lt;strong&gt;three tables&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sales&lt;/strong&gt;: the main fact table (orders)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Products&lt;/strong&gt;:  product dimension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locations&lt;/strong&gt;: store/location dimensions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benchmark consists of &lt;a href="https://github.com/JosueBogran/coffeeshopdatageneratorv2/blob/2a99993b6bca94c0bc04fae7c695e86cd152add1/Performance%20Test%20Queries.sql" rel="noopener noreferrer"&gt;17 SQL queries&lt;/a&gt;, most involving &lt;strong&gt;joins&lt;/strong&gt; between the fact and one or both dimension tables. All queries were run &lt;strong&gt;sequentially&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Part 1 of the original benchmark covered the smaller 721 million row scale, and part 2 added results for the 1.4 billion and 7.2 billion row scales.&lt;/p&gt;

&lt;p&gt;This post mirrors the structure and layout of the original benchmark posts: same queries, same chart style, same order, just re-run on ClickHouse Cloud. For each scale, we report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total cost&lt;/strong&gt; (USD)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total runtime&lt;/strong&gt; (seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per query&lt;/strong&gt; (excluding Q10 &amp;amp; Q16)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seconds per query&lt;/strong&gt; (excluding Q10 &amp;amp; Q16)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per query&lt;/strong&gt; (Q10 &amp;amp; Q16 only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seconds per query&lt;/strong&gt; (Q10 &amp;amp; Q16 only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queries 10 and 16 are significantly slower than the other queries and would compress the scale of the other queries on the charts. That’s why the original posts listed them separately.&lt;/p&gt;

&lt;p&gt;The original benchmark included both “Clustered” and “Non-clustered” variants. (Here, “Clustered” means the data was physically sorted and co-located to improve query performance, especially on large tables.) For consistency, we report only the &lt;strong&gt;Clustered&lt;/strong&gt; results here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Methodology &amp;amp; setup:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;In the original benchmark results appear to be hot runs. We run each query 5 times and report the fastest run, to reflect warm-cache performance fairly.&lt;br&gt;&lt;br&gt;All results shown here are based on the ClickHouse Cloud &lt;a href="https://clickhouse.com/pricing?plan=scale&amp;amp;provider=aws&amp;amp;region=us-east-1&amp;amp;hours=8&amp;amp;storageCompressed=false" rel="noopener noreferrer"&gt;Scale tier&lt;/a&gt;, using services deployed on AWS in the us-east-2 region. The &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/tree/main/clickhouse-cloud/results" rel="noopener noreferrer"&gt;full results&lt;/a&gt; also include Enterprise tier costs, which still compare favorably.&lt;br&gt;&lt;br&gt;All services were running ClickHouse 25.4.1, with parallel replicas enabled by default.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’ll find the full chart sets below, presented without further commentary. All context is in the original LinkedIn posts, and we’ll wrap up with a clear takeaway.&lt;/p&gt;

&lt;p&gt;In the charts below, ClickHouse Cloud results follow the label format &lt;code&gt;CH 2n_30c_120g&lt;/code&gt;, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2n&lt;/code&gt; = number of compute nodes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;30c&lt;/code&gt; = CPU cores per node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;120g&lt;/code&gt; = RAM per node (in GB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All service configurations use 30 cores and 120 GB RAM per node, with 4 service sizes tested: 2, 4, 8, and 16 compute nodes.&lt;/p&gt;

&lt;p&gt;The label meanings for Databricks (e.g. &lt;code&gt;DBX_S&lt;/code&gt;) and Snowflake (e.g. &lt;code&gt;SF_S_Gen2&lt;/code&gt;) are unchanged and documented in the original posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: 500m scale (721m rows)
&lt;/h2&gt;

&lt;p&gt;This scale uses a seed of 500m orders to generate a total of 721m rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Total cost
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_500m_f80a9e8196.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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_500m_f80a9e8196.png" alt="total_cost_500m.png" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total runtime for all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Total runtime
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_500m_e8089e6f24.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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_500m_e8089e6f24.png" alt="total_perf_500m.png" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total cost for running all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cost per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_500m_4b671d6b7d.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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_500m_4b671d6b7d.png" alt="cost_excl_q10_q16_500m.png" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_500m_1a59901ab6.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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_500m_1a59901ab6.png" alt="perf_excl_q10_q16_500m.png" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2F655s5bjf2j3u9pl99t86.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%2F655s5bjf2j3u9pl99t86.png" alt="cost_q10_q16_500m.png" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2Fswbs6iw3plqhi3e61gvr.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%2Fswbs6iw3plqhi3e61gvr.png" alt="perf_q10_q16_500m.png" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: 1b scale (1.4b rows)
&lt;/h2&gt;

&lt;p&gt;This scale uses a seed of 1b orders to generate a total of 1.4b rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Total cost
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_1b_4f6f1ce578.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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_1b_4f6f1ce578.png" alt="total_cost_1b.png" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total runtime for all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Total runtime
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_1b_5d6410220b.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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_1b_5d6410220b.png" alt="total_perf_1b.png" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total cost for running all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cost per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_1b_f7db403624.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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_1b_f7db403624.png" alt="cost_excl_q10_q16_1b.png" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_1b_82f8f0e2f9.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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_1b_82f8f0e2f9.png" alt="perf_excl_q10_q16_1b.png" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2Fef0wuvtfdfrhpkyu2kab.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%2Fef0wuvtfdfrhpkyu2kab.png" alt="cost_q10_q16_1b.png" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2Fdgdfl7tukj8513w6wtb8.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%2Fdgdfl7tukj8513w6wtb8.png" alt="perf_q10_q16_1b.png" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: 5b scale (7.2b rows)
&lt;/h2&gt;

&lt;p&gt;This scale uses a seed of 5b orders to generate a total of 7.2b rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Total cost
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_5b_343416146f.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%2Fclickhouse.com%2Fuploads%2Ftotal_cost_5b_343416146f.png" alt="total_cost_5b.png" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total runtime for all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Total runtime
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_5b_149633e506.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%2Fclickhouse.com%2Fuploads%2Ftotal_perf_5b_149633e506.png" alt="total_perf_5b.png" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each bar also shows the total cost for running all 17 queries, shown in parentheses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cost per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_5b_18f629315e.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%2Fclickhouse.com%2Fuploads%2Fcost_excl_q10_q16_5b_18f629315e.png" alt="cost_excl_q10_q16_5b.png" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (excluding Q10 &amp;amp; Q16)
&lt;/h3&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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_5b_2a25e25670.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%2Fclickhouse.com%2Fuploads%2Fperf_excl_q10_q16_5b_2a25e25670.png" alt="perf_excl_q10_q16_5b.png" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2Fs5sa4a5fojyms9xwn30m.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%2Fs5sa4a5fojyms9xwn30m.png" alt="cost_q10_q16_5b.png" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime per query (Q10 &amp;amp; Q16 only)
&lt;/h3&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%2Fjv5euwd9fg0d7sdlt058.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%2Fjv5euwd9fg0d7sdlt058.png" alt="perf_q10_q16_5b.png" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned and what’s next
&lt;/h2&gt;

&lt;p&gt;ClickHouse is fast with joins. Really fast, across all scales.&lt;/p&gt;

&lt;p&gt;The 17 queries in this benchmark focus on practical join workloads: 2–3 tables, no tuning, no rewrites. We ran them as-is to see how ClickHouse stacks up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At 500m scale&lt;/strong&gt;, most queries complete in &lt;strong&gt;under 1 second&lt;/strong&gt;, with ClickHouse consistently &lt;strong&gt;3–5× faster&lt;/strong&gt; than the alternatives. And cheaper.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At 1b scale&lt;/strong&gt;, ClickHouse joins, aggregates, and sorts &lt;strong&gt;1.7 billion rows in &lt;a href="https://github.com/sdairs/coffeeshop-benchmark/blob/e1837073012e69da25845d45c90a21fa215c2c13/clickhouse-cloud/results/result_v25_4_1_1b_16n_30c_120g_20250620_141435.json#L132" rel="noopener noreferrer"&gt;just half a second&lt;/a&gt;&lt;/strong&gt;, while other systems need &lt;strong&gt;5 to 13 seconds&lt;/strong&gt;, and still cost more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At 5b scale&lt;/strong&gt;, even the heaviest queries finish in &lt;strong&gt;seconds, not minutes&lt;/strong&gt;, with ClickHouse staying the fastest and cheapest option overall.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We didn’t do anything special to get these results, no config tweaks, no ClickHouse-specific tricks. Just a clean run of the original benchmark. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In &lt;a href="https://clickhouse.com/blog/join-me-if-you-can-clickhouse-vs-databricks-snowflake-part-2" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;, we’ll show you how to make it truly fast, the ClickHouse way, with a few powerful tricks up our sleeve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Behind the scenes, we’ve spent the last 6 months making joins in ClickHouse much faster and more scalable, from improved planning and memory efficiency to better execution strategies.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;And we’re not stopping here.&lt;/p&gt;

&lt;p&gt;Next, we’re turning up the difficulty: full &lt;a href="https://clickhouse.com/docs/getting-started/example-datasets/tpch" rel="noopener noreferrer"&gt;TPC-H&lt;/a&gt;, up to 8-way joins.&lt;/p&gt;

&lt;p&gt;Want to see how ClickHouse handles the most demanding joins? Stay tuned for our TPC-H results.&lt;/p&gt;

</description>
      <category>data</category>
      <category>database</category>
      <category>dataengineering</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Integrating with ClickHouse MCP</title>
      <dc:creator>ClickHouse</dc:creator>
      <pubDate>Tue, 01 Jul 2025 11:03:08 +0000</pubDate>
      <link>https://dev.to/clickhouse/integrating-with-clickhouse-mcp-461e</link>
      <guid>https://dev.to/clickhouse/integrating-with-clickhouse-mcp-461e</guid>
      <description>&lt;p&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; is a protocol for connecting third-party services - databases, APIs, tools, etc. - to LLMs. Creating an MCP server defines how a client can interact with your service. An MCP client (like Claude Desktop, ChatGPT, Cursor, Windsurf, and more) connects to the server, and allows an LLM to interact with your service. MCP is quickly becoming the de-facto protocol, and we published the ClickHouse MCP server earlier in the year: &lt;a href="https://github.com/ClickHouse/mcp-clickhouse" rel="noopener noreferrer"&gt;mcp-clickhouse&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;Natural language interfaces are becoming popular across pretty much all domains, including the spaces where we find ClickHouse users. Software engineers, data engineers, analytics engineers, you name it. We're all starting to adopt natural language and agentic interfaces for parts of the job. It's making it easier than ever to work with data, whether you're comfortable with SQL or not. What we're seeing is that LLMs are helping to round out and expand people's skills - software engineers can do more with data, data engineers can do more with software, etc. There's never been a time when a wider audience could work with data.&lt;/p&gt;

&lt;p&gt;Universally across these users, domains, and interfaces is the expectation of speed and interactivity in the user experience. Users aren't firing off a query on Friday afternoon, grabbing a delicious Bánh mì on the way home, and picking up a report on Monday morning. They're having a collaborative, interactive conversation with an LLM, where responses are delivered in seconds, and there is a real back-and-forth. If we add third-party services into the mix, we can't disrupt the user experience. If a user wants to query their database this way, it needs to handle this kind of responsiveness.&lt;/p&gt;

&lt;p&gt;That's what makes ClickHouse the ideal database for agentic AI data workflows. ClickHouse is built to be the world's fastest analytical database, where no bits, bytes, or milliseconds are wasted. Even before the LLM and agentic era, ClickHouse aimed to support interactive analytics at scale. We didn't set out to be the best database for agentic AI - sometimes, happy accidents just happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future use cases
&lt;/h2&gt;

&lt;p&gt;Popularity aside, it's still early days, and the tools, workflows, and use cases are evolving rapidly. We see a lot of people forgoing the traditional SQL interface and BI tooling, instead using chat interfaces like Claude Desktop or ChatGPT to talk to their data, skipping SQL entirely, and generating insights and visualizations. We also see developers without a traditional data background building user-facing applications that expose data to end users, relying on LLMs not just to generate front-ends, but to structure data and optimise queries for very high concurrency.&lt;/p&gt;

&lt;p&gt;With ClickHouse also becoming &lt;a href="https://clickhouse.com/blog/clickstack-a-high-performance-oss-observability-stack-on-clickhouse" rel="noopener noreferrer"&gt;the best choice for observability 2.0&lt;/a&gt;, we're seeing SREs and DevOps teams using LLMs to query their traces, metrics, and logs, blending full-text search and analytics without obscure query syntax. &lt;/p&gt;

&lt;p&gt;And we're imagining what might come next: perhaps we'll see LLMs able to use existing observability data to inform their thinking, perhaps making recommendations for architecture, performance enhancements, or bug fixes based on the data they can access without requiring users to prompt with specific errors or traces.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Soon, ClickHouse Cloud will offer a remote MCP server as a default interface. That means any MCP client could connect directly to your cloud instance without additional local setup.&lt;/p&gt;

&lt;p&gt;Want early access? &lt;a href="http://clickhouse.ai" rel="noopener noreferrer"&gt;Sign up for the AI features waitlist at clickhouse.ai&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ClickHouse MCP Agent Examples
&lt;/h2&gt;

&lt;p&gt;To make it dead simple to get started, we’ve put together some practical examples showing how to integrate various libraries with the ClickHouse MCP server. &lt;/p&gt;

&lt;p&gt;You can do this today with the open-source &lt;a href="https://github.com/ClickHouse/mcp-clickhouse" rel="noopener noreferrer"&gt;mcp-clickhouse server&lt;/a&gt;. For more on how this fits into the bigger picture, check out &lt;a href="https://clickhouse.com/blog/agenthouse-demo-clickhouse-llm-mcp" rel="noopener noreferrer"&gt;this AgentHouse demo&lt;/a&gt; and our thoughts on &lt;a href="https://clickhouse.com/blog/agent-facing-analytics" rel="noopener noreferrer"&gt;agent-facing analytics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can find all five in the &lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp" rel="noopener noreferrer"&gt;ClickHouse/examples repo&lt;/a&gt;. They are all configured to run against the &lt;a href="https://sql.clickhouse.com/" rel="noopener noreferrer"&gt;ClickHouse SQL Playground&lt;/a&gt;, which is configured via the following config:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
env = {
    "CLICKHOUSE_HOST": "sql-clickhouse.clickhouse.com",
    "CLICKHOUSE_PORT": "8443",
    "CLICKHOUSE_USER": "demo",
    "CLICKHOUSE_PASSWORD": "",
    "CLICKHOUSE_SECURE": "true"
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We also use Anthropic models and have provided our API key via the &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Agno
&lt;/h3&gt;

&lt;p&gt;Let’s start with &lt;a href="https://docs.agno.com/tools/mcp/mcp#multiple-mcp-servers" rel="noopener noreferrer"&gt;Agno&lt;/a&gt; (previously PhiData), a lightweight, high-performance library for building Agents.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
async with MCPTools(command="uv run --with mcp-clickhouse --python 3.13 mcp-clickhouse", env=env, timeout_seconds=60) as mcp_tools:
    agent = Agent(
        model=Claude(id="claude-3-5-sonnet-20240620"),
        markdown=True, 
        tools = [mcp_tools]
    )
    await agent.aprint_response("What's the most starred project in 2025?", stream=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This one has a straightforward API. We initialize &lt;code&gt;MCPTools&lt;/code&gt; with the command to launch our local MCP Server, and all the tools become available via the &lt;code&gt;mcp_tools&lt;/code&gt; variable. We can then pass the tools into our agent before calling it on the last line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp/agno" rel="noopener noreferrer"&gt;View the full Agno example&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. DSPy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dspy.ai/" rel="noopener noreferrer"&gt;DSPy&lt;/a&gt; is a framework from Stanford for programming language models.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
server_parameters = StdioServerParameters(
    command="uv",
    args=[
        'run',
        '--with', 'mcp-clickhouse',
        '--python', '3.13',
        'mcp-clickhouse'
    ],
    env=env
)

dspy.configure(lm=dspy.LM("anthropic/claude-sonnet-4-20250514"))

class DataAnalyst(dspy.Signature):
    """You are a data analyst. You'll be asked questions and you need to try to answer them using the tools you have access to. """

    user_request: str = dspy.InputField()
    process_result: str = dspy.OutputField(
        desc=(
            "Answer to the query"
        )
    )

async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()

        dspy_tools = []
        for tool in tools.tools:
            dspy_tools.append(dspy.Tool.from_mcp_tool(session, tool))

        print("Tools", dspy_tools)

        react = dspy.ReAct(DataAnalyst, tools=dspy_tools)
        result = await react.acall(user_request="What's the most popular Amazon product category")
        print(result)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This one is more complicated. We similarly initialize our MCP server, but rather than having a single command as a string, we need to split up the command and the arguments. &lt;/p&gt;

&lt;p&gt;DSPy also requires us to specify a &lt;code&gt;Signature&lt;/code&gt; class for each interaction, where we define input and output fields. We then provide that class when initializing our agent, which is done using the &lt;code&gt;React&lt;/code&gt; class. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReAct&lt;/code&gt; stands for "reasoning and acting," which asks the LLM to decide whether to call a tool or wrap up the process. If a tool is required, the LLM takes responsibility for deciding which tool to call and providing the appropriate arguments.&lt;/p&gt;

&lt;p&gt;You’ll notice that we must iterate over our MCP tools and convert them to DSPy ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp/dspy" rel="noopener noreferrer"&gt;View the full DSPy example&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. LangChain
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langchain-mcp-adapters" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; is a framework for building LLM-powered applications.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
server_params = StdioServerParameters(
    command="uv", 
    args=[
        "run", 
        "--with", "mcp-clickhouse",
        "--python", "3.13", 
        "mcp-clickhouse"
    ],
    env=env
)
         
async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await load_mcp_tools(session)
        agent = create_react_agent("anthropic:claude-sonnet-4-0", tools)
        
        handler = UltraCleanStreamHandler()        
        async for chunk in agent.astream_events(
            {"messages": [{"role": "user", "content": "Who's committed the most code to ClickHouse?"}]}, 
            version="v1"
        ):
            handler.handle_chunk(chunk)
            
        print("\n")
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;LangChain follows a similar approach to DSPy when initializing the MCP Server. Like DSPy, we need to invoke a ReAct function to create the agent, passing in our MCP tools. We (well, Claude!) wrote a custom bit of code (&lt;code&gt;UltaCleanStreamHandler&lt;/code&gt;) to render the output in a more user-friendly way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp/langchain" rel="noopener noreferrer"&gt;View the full LangChain example&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. LlamaIndex
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.llamaindex.ai/en/stable/api_reference/tools/mcp/" rel="noopener noreferrer"&gt;LlamaIndex&lt;/a&gt; is a data framework for your LLM applications.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
mcp_client = BasicMCPClient(
    "uv", 
    args=[
        "run", 
        "--with", "mcp-clickhouse",
        "--python", "3.13", 
        "mcp-clickhouse"
    ],
    env=env
)

mcp_tool_spec = McpToolSpec(
    client=mcp_client,
)

tools = await mcp_tool_spec.to_tool_list_async()

agent_worker = FunctionCallingAgentWorker.from_tools(
    tools=tools, 
    llm=llm, verbose=True, max_function_calls=10
)
agent = AgentRunner(agent_worker)

response = agent.query("What's the most popular repository?")
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;LlamaIndex follows the familiar approach of initializing the MCP server. We then initialize an agent with our tools and LLM. We found the default &lt;code&gt;max_function_calls&lt;/code&gt; value of 5 was too low and wasn’t enough to answer any questions, so we increased it to 10.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp/llamaindex" rel="noopener noreferrer"&gt;View the full LlamaIndex example&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. PydanticAI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ai.pydantic.dev/mcp/run-python/#installation" rel="noopener noreferrer"&gt;PydanticAI&lt;/a&gt; is a Python agent framework designed to make it less painful to build production-grade applications with Generative AI.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;
server = MCPServerStdio(  
    'uv',
    args=[
        'run',
        '--with', 'mcp-clickhouse',
        '--python', '3.13',
        'mcp-clickhouse'
    ],
    env=env
)
agent = Agent('anthropic:claude-sonnet-4-0', mcp_servers=[server])

async with agent.run_mcp_servers():
    result = await agent.run("Who's done the most PRs for ClickHouse?")
    print(result.output)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pydantic has the simplest API. Again, we initialize our MCP server and pass it into the agent. It then runs the server as an asynchronous context manager and we can ask the agent questions inside that block.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ClickHouse/examples/tree/main/ai/mcp/pydanticai" rel="noopener noreferrer"&gt;View the full PydanticAI example&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;We’re just getting started with MCP and ClickHouse, and we’d love to hear about what you’re building and your experience using mcp-clickhouse. &lt;/p&gt;

&lt;p&gt;Try out the examples, build something cool, and let us know what you think. If you run into issues or have ideas, open a GitHub issue or &lt;a href="https://clickhouse.com/slack" rel="noopener noreferrer"&gt;chat with us in Slack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>database</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Scaling our Observability platform beyond 100 Petabytes</title>
      <dc:creator>ClickHouse</dc:creator>
      <pubDate>Thu, 26 Jun 2025 17:05:06 +0000</pubDate>
      <link>https://dev.to/clickhouse/scaling-our-observability-platform-beyond-100-petabytes-1kip</link>
      <guid>https://dev.to/clickhouse/scaling-our-observability-platform-beyond-100-petabytes-1kip</guid>
      <description>&lt;blockquote&gt;
&lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt; &lt;strong&gt;Observability at scale:&lt;/strong&gt; Our internal system grew from 19 PiB to 100 PB of uncompressed logs and from ~40 trillion to 500 trillion rows.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Efficiency breakthrough:&lt;/strong&gt; We absorbed a 20× surge in event volume using under 10% of the CPU previously needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OTel pitfalls:&lt;/strong&gt; The required parsing and marshalling of events in OpenTelemetry proved a bottleneck and didn’t scale - our custom pipeline addressed this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Introducing HyperDX:&lt;/strong&gt; ClickHouse-native observability UI for seamless exploration, correlation, and root-cause analysis with Lucene-like syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;About a year ago, we &lt;a href="https://clickhouse.com/blog/building-a-logging-platform-with-clickhouse-and-saving-millions-over-datadog" rel="noopener noreferrer"&gt;shared the story of LogHouse&lt;/a&gt;- our internal logging platform built to monitor ClickHouse Cloud. At the time, it managed what felt like a massive 19 PiB of data. More than just solving our observability challenges, LogHouse also saved us millions by replacing an increasingly unsustainable Datadog bill. The response to that post was overwhelming. It was clear our experience resonated with others facing similar struggles with traditional observability vendors and underscored just how critical effective data management is at scale.&lt;/p&gt;

&lt;p&gt;A year later, LogHouse has grown beyond anything we anticipated and is now storing over 100 petabytes of uncompressed data across nearly 500 trillion rows. That kind of scale forced a series of architectural changes, new tools, and hard-earned lessons that we felt were worth sharing - not least that OpenTelemetry (OTel) isn’t always the panacea of Observability (though we still love it), and that sometimes custom pipelines are essential. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In our case, this shift enabled us to handle a 20x increase in event volume using less than 10% of the CPU for our most critical data source - a transformation with massive implications for cost and efficiency.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Other parts of our stack have also changed, not least due to the &lt;a href="https://clickhouse.com/blog/clickhouse-acquires-hyperdx-the-future-of-open-source-observability" rel="noopener noreferrer"&gt;ClickHouse acquisition of HyperDX&lt;/a&gt;. Not only did this give us a first-party ClickHouse-native UI, but it also led to the &lt;a href="https://clickhouse.com/blog/clickstack-a-high-performance-oss-observability-stack-on-clickhouse" rel="noopener noreferrer"&gt;creation of ClickStack &lt;/a&gt;- an opinionated, end-to-end observability stack built around ClickHouse. With HyperDX, we’ve started transitioning away from our Grafana-based custom UI, moving toward a more integrated experience for exploration, correlation, and root cause analysis.&lt;/p&gt;

&lt;p&gt;As more teams adopt ClickHouse for observability and realize just how much they can store and query affordably, we hope these insights prove as useful as our first post. If you’re curious about this journey, when and where OTel is appropriate, and how we scaled a log pipeline to 100PB…read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond general purpose: evolving observability at scale
&lt;/h2&gt;

&lt;p&gt;Over the past year, our approach to observability has undergone a significant transformation. We've continued to leverage OpenTelemetry to gather general-purpose logs, but as our systems have scaled, we began to reach its limits. While OTel remains a valuable part of our toolkit, it couldn't fully deliver the performance and precision we needed for our most demanding workloads. This prompted us to develop purpose-built tools tailored to our critical systems and rethink where generic solutions truly fit. Along the way, we've broadened the range of data we collect and revamped how we present insights to engineers.&lt;/p&gt;

&lt;h3&gt;
  
  
  A new frontier of scale
&lt;/h3&gt;

&lt;p&gt;When we last wrote about LogHouse, we were proud to handle 19 PiB of uncompressed data across 37 trillion rows. Today, those numbers feel like a distant memory. LogHouse now stores over 100 petabytes of uncompressed data, representing nearly 500 trillion rows.&lt;br&gt;
Here's a quick look at the breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Uncompressed Size&lt;/th&gt;
&lt;th&gt;Stored rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SysEx&lt;/td&gt;
&lt;td&gt;93.6 PB&lt;/td&gt;
&lt;td&gt;431 Trillion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTel&lt;/td&gt;
&lt;td&gt;14.5 PB&lt;/td&gt;
&lt;td&gt;16.7 Trillion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers also tell a story. In our original post, 100% of our telemetry flowed through OpenTelemetry, with every log line collected via the same general-purpose pipeline. But as the scale and complexity of our data grew, so did the need for specialization.&lt;br&gt;
While our total volume has grown more than 5x, the breakdown reveals a deliberate shift in strategy: today, the vast majority of our data comes from “SysEx”, a new purpose-built exporter we developed to handle high-throughput, high-fidelity system logs from ClickHouse itself. This shift marks a turning point in how we think about observability pipelines - and brings us to our first key topic.&lt;/p&gt;

&lt;p&gt;We hope the following helps comprehend the scale at which LogHouse operates.&lt;/p&gt;


&lt;h3&gt;
  
  
  OpenTelemetry's efficiency challenges at extreme scale
&lt;/h3&gt;

&lt;p&gt;Initially, we used OpenTelemetry (OTel) for all log collection. It was a great starting point and an established industry standard which allowed us to quickly establish a baseline where every pod in our Kubernetes environment shipped logs to ClickHouse. However, as we scaled, we identified two key reasons to build a specialized tool for shipping our core ClickHouse server telemetry.&lt;/p&gt;

&lt;p&gt;First, while OTel capably captured the ClickHouse text log via stdout, this represents only a narrow slice of the telemetry ClickHouse exposes. Any ClickHouse expert knows that the real gold lies in its &lt;strong&gt;system tables&lt;/strong&gt; - a rich, structured collection of logs, metrics, and operational insights that go far beyond what’s printed to standard output. These tables capture everything from query execution details to disk I/O and background task states, and unlike ephemeral logs, they can be retained indefinitely within a cluster. For both real-time debugging and historical analysis, this data is invaluable. We wanted all of it in LogHouse.&lt;/p&gt;

&lt;p&gt;Second, the inefficiency of the OTel pipeline for this specific task became obvious as we scaled. &lt;/p&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%2Fclickhouse.com%2Fuploads%2Floghouse_041bc14329.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%2Fclickhouse.com%2Fuploads%2Floghouse_041bc14329.png" alt="loghouse.png" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data journey involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A customer's ClickHouse instance writes logs as JSON to stdout.&lt;/li&gt;
&lt;li&gt;The kubelet persists these logs in &lt;code&gt;/var/log/…&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An OTel collector collects these logs from the disk, parsing and marshalling the JSON into an in-memory representation.&lt;/li&gt;
&lt;li&gt;The collector transforms these into the OTel log format - again an in-memory representation.&lt;/li&gt;
&lt;li&gt;Finally, they are inserted back into another ClickHouse instance (LogHouse) over the native format (requiring another transformation within the ClickHouse Go client).&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The architecture described here is simplified. In reality, our OTel pipeline is more involved. Logs were first collected at the edge in JSON, converted into the OTel format, and sent over OTLP to a set of gateway instances. These gateways (also OTel collectors) performed additional processing before finally converting the data into ClickHouse’s native format for ingestion. Each step introduced overhead, latency, and further complexity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At our scale, this pipeline introduced two critical problems: inefficiency and data loss. First, we were burning substantial compute on repeated data transformations. Native ClickHouse types were being flattened into JSON, mapped into the OTel log format, and then re-ingested - only to be reinterpreted by ClickHouse on the other end. This not only wasted CPU cycles but also degraded the fidelity of the data.&lt;br&gt;
Even more importantly, we were hitting hard resource limits on the collectors themselves. Deployed as agents on each Kubernetes node, they were subject to strict CPU and memory constraints via standard Kubernetes limits. As traffic spiked, many collectors ran so hot they began dropping log lines outright - unable to keep up with the volume emitted by ClickHouse. We were losing data at the edge before it ever had a chance to reach LogHouse.&lt;br&gt;
We found ourselves at a crossroads: either dramatically scale up the resource footprint of our OTel agents (and gateways) or rethink the entire ingestion model. We chose the latter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: To put the cost in perspective - handling 20 million rows per second through the OpenTelemetry pipeline without dropping events would require an estimated 8,000 CPU cores across agents and collectors. That’s an enormous footprint dedicated solely to log collection, making it clear that the general-purpose approach was unsustainable at our scale.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Building SysEx: A specialized system for ClickHouse-to-ClickHouse transfers
&lt;/h3&gt;

&lt;p&gt;Our solution was to develop the &lt;strong&gt;System Tables Exporter&lt;/strong&gt;, or &lt;strong&gt;SysEx&lt;/strong&gt;. This is a specialized tool designed to transfer data from one ClickHouse instance to another as efficiently as possible. We wanted to go directly from the system tables in a customer's pod to the tables in LogHouse, preserving native ClickHouse types and eliminating all intermediate conversions. This has the fantastic side benefit that any query our engineers use to troubleshoot a live instance can be trivially adapted to query historical data across our entire fleet in LogHouse, as the table schemas are identical, with the addition of some enrichment columns (such as the Pod Name, ClickHouse version, etc). &lt;/p&gt;

&lt;p&gt;Firstly we should emphasize that SysEx performs a &lt;strong&gt;literal byte-for-byte copy&lt;/strong&gt; of data from the source to the destination. This preserves full fidelity, eliminates unnecessary CPU overhead, and avoids the pitfalls of repeated marshalling.&lt;/p&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%2Fclickhouse.com%2Fuploads%2Floghouse_diagram_5e4e1a0646.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%2Fclickhouse.com%2Fuploads%2Floghouse_diagram_5e4e1a0646.png" alt="loghouse-diagram.png" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture is simple and powerful. We run a pool of SysEx scrapers connecting to our customer's ClickHouse instances. A hash ring assigns each customer pod to a specific scraper replica to distribute the load. These scrapers then run SELECT queries against the source pod's system tables and stream the data directly into LogHouse, without any deserialization. The scrapers simply coordinate and forward bytes between the source and destination.&lt;br&gt;
Scraping system tables requires careful handling to ensure no data is missed due to buffer flushes. Fortunately, nearly all system table data is inherently time-series in nature. SysEx leverages this by querying within a sliding time window, deliberately trailing real time by a small buffer - typically five minutes. This delay allows for any internal buffers to flush, ensuring that when a scraper queries a node, all relevant rows for that time window are present and complete. This strategy has proven reliable and meets our internal SLAs for timely and complete event delivery to LogHouse.&lt;/p&gt;

&lt;p&gt;SysEx is written in Go, like most of our infrastructure components for ClickHouse Cloud. Naturally, this raises a question for anyone familiar with the Go ClickHouse client: how do we avoid the built-in marshalling and unmarshalling of data when reading from and writing to ClickHouse? By default, the client converts data into Go-native types, which would defeat the purpose of a byte-for-byte copy. To solve this, we &lt;a href="https://github.com/ClickHouse/clickhouse-go/pull/1233" rel="noopener noreferrer"&gt;contributed improvements&lt;/a&gt; to the Go client that allow us to &lt;strong&gt;bypass internal marshalling entirely&lt;/strong&gt;, enabling SysEx to stream data in its native format directly from the source cluster to LogHouse - without decoding, re-encoding, or allocating intermediary data structures. &lt;/p&gt;

&lt;p&gt;This approach is broadly equivalent to a simple bash command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'default:&amp;lt;password&amp;gt;'&lt;/span&gt; &lt;span class="s2"&gt;"https://sql-clickhouse.clickhouse.com:8443/?query=SELECT+*+FROM+system.query_log+FORMAT+Native"&lt;/span&gt; | curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @- &lt;span class="s1"&gt;'http://localhost:8123/?query=INSERT+INTO+query_log+FORMAT+Native'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An actual go implementation for the curious can be found &lt;a href="https://pastila.nl/?00740eb5/722157a76fc25f54212d7097b805253b#OSfprbH3wM1dGgtyMAnmuw==" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most importantly, SysEx doesn’t require the heavy buffering that OTel does, thanks to its pull-based model. Because scrapers query data at a steady, controlled rate, we don’t risk dropping logs when LogHouse is temporarily unavailable or when the source experiences a spike in telemetry. Instead, SysEx naturally handles backfill by scraping historical windows, ensuring reliable delivery without overloading the system or requiring complex retry buffers.&lt;/p&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%2Fr1a0fw5ej54vwateu9nd.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%2Fr1a0fw5ej54vwateu9nd.png" alt="pipeline.png" width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic schema generation
&lt;/h3&gt;

&lt;p&gt;One of the key challenges with the SysEx approach is that it assumes the source and target schemas match. But in reality, as any ClickHouse user knows, system table schemas change frequently. Engineers continuously add new metrics and columns to support emerging features and accelerate issue diagnosis, which means the schema is a moving target. &lt;/p&gt;

&lt;p&gt;To handle this, we generate schemas dynamically. When SysEx encounters a system table, it inspects and hashes its schema to determine if a matching table already exists in LogHouse. If it does, the data is inserted there. If not, a new schema version is created for this system table e.g. &lt;code&gt;text_log_6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At query time, we use ClickHouse’s &lt;a href="https://clickhouse.com/blog/clickhouse-release-25-01#better-merge-tables" rel="noopener noreferrer"&gt;Merge table engine&lt;/a&gt; to unify all schema iterations into a single logical view. This allows us to query across multiple versions of a system table seamlessly. The engine automatically resolves schema differences by selecting only the columns that are compatible across tables, or by restricting the query to tables that contain the requested columns. This gives us forward compatibility as schemas evolve, without sacrificing query simplicity or requiring manual schema management.&lt;/p&gt;

&lt;h3&gt;
  
  
  State snapshotting
&lt;/h3&gt;

&lt;p&gt;As we continued to scale and refine our observability capabilities, one of our primary focuses was capturing in-memory system tables, such as &lt;code&gt;system.processes&lt;/code&gt;. Unlike the time-series data we’ve been capturing, these tables provide a snapshot of the server’s state at a specific point in time. To handle this, we implemented a periodic snapshot process, capturing these in-memory tables and storing them in LogHouse.&lt;/p&gt;

&lt;p&gt;This approach not only allows us to capture the state of the cluster at any given moment, but also provides time-travel through critical details like table schemas and cluster settings. With this additional data, we are able to enhance our diagnostic capabilities by performing cluster-wide or ClickHouse Cloud-wide analyses. This we can join against service settings or query characteristics like used_functions to pinpoint anomalies, making it easier to identify the root causes of issues as they arise. By correlating queries with particular schemas, we further improved our ability to proactively identify and resolve performance or reliability problems for our customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fleet-wide queries
&lt;/h3&gt;

&lt;p&gt;One of the many powerful capabilities we've unlocked with SysEx is the ability to take the same &lt;a href="https://clickhouse.com/blog/common-issues-you-can-solve-using-advanced-monitoring-dashboards" rel="noopener noreferrer"&gt;Advanced Dashboard queries&lt;/a&gt;  that customers use to monitor their individual ClickHouse instances and run them across our entire fleet of customer instances simultaneously.&lt;/p&gt;

&lt;p&gt;For release analysis, we can now execute proven diagnostic queries before and after deployments to immediately identify behavioral changes across our entire fleet. This has been rolled into our comprehensive release analysis process. Queries that analyze query performance patterns, resource utilization trends, and error rates complete in real time, allowing us to quickly spot regressions or validate improvements at fleet scale. &lt;/p&gt;

&lt;p&gt;Secondly, our support dashboards can now embed the same deep diagnostic queries that customers rely on, but with enriched context from our centralized telemetry. When investigating customer issues, support engineers can run familiar Advanced Dashboard queries while simultaneously correlating with network logs, Kubernetes events, data and control plane events - all within the same interface.&lt;/p&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%2Fclickhouse.com%2Fuploads%2Fgrafana_zoom_67ef33f55d.gif" 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%2Fclickhouse.com%2Fuploads%2Fgrafana_zoom_67ef33f55d.gif" alt="grafana zoom" width="120" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  20x more data, 90% less CPU: The numbers behind our rewrite
&lt;/h3&gt;

&lt;p&gt;The efficiency gains from this SysEx are staggering. Consider these stats from LogHouse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OTel Collectors:&lt;/strong&gt; Use over 800 CPU cores to ship 2 million logs per second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LogHouse Scrapers (SysEx):&lt;/strong&gt; Use just 70 CPU cores to ship 37 million logs per second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This specialized approach has allowed us to handle a 20x increase in event volume with less than 10 percent of the CPU footprint for our most important data source. Most importantly, it means we no longer drop events at the edge. To achieve this same level of reliability with our previous OTel-based pipeline, &lt;strong&gt;we would have needed over 8,000 CPU cores&lt;/strong&gt;. SysEx delivers it with a fraction of the resources, maintaining full fidelity and consistent delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  When OpenTelemetry is the the right choice
&lt;/h2&gt;

&lt;p&gt;If you’ve read this far, you might be wondering: when is OpenTelemetry still the right choice, and is it still useful?&lt;br&gt;
We firmly believe that it is. While our architecture has evolved to meet challenges at extreme scale, such as parsing and processing over 20 million log lines per second, OpenTelemetry remains a critical part of our stack. It offers a standardized, vendor-neutral format and provides an excellent onboarding experience for new users - and is hence the default choice for ClickStack. Unlike SysEx, which is tightly integrated with ClickHouse internals, OpenTelemetry decouples producers from consumers, which is a major architectural advantage, especially for users who want flexibility across observability platforms.&lt;/p&gt;

&lt;p&gt;It is also well suited for scenarios where SysEx cannot operate. SysEx is pull-based and relies on querying live system tables, which means the service must be healthy and responsive. If a service is crash-looping or down, SysEx is unable to scrape data because the necessary system tables are unavailable. OpenTelemetry, by contrast, operates in a passive fashion. It captures logs emitted to &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt;, even when the service is in a failed state. This allows us to collect logs during incidents and perform root cause analysis even if the service never became fully healthy.&lt;br&gt;
For this reason, we continue to run OpenTelemetry across all ClickHouse services. The key difference is in what we collect. Previously, we ingested everything, including trace-level logs. Now, we collect only info-level and above. This significantly reduces the data volume and allows our OTel collectors and gateways to operate with far fewer resources. The result is a smaller, more focused pipeline that still accounts for the 2 million log lines per second referenced earlier.&lt;/p&gt;
&lt;h2&gt;
  
  
  HyperDX for better experience
&lt;/h2&gt;

&lt;p&gt;Collecting all this data is just the beginning. Making it usable and accessible is what really matters. In the first iteration of LogHouse, we built a highly customized observability experience on top of Grafana. It served us well, but as our internal data sources grew and diversified, particularly with the introduction of SysEx and wide-column telemetry, it became clear we needed something more deeply integrated with ClickHouse.&lt;/p&gt;

&lt;p&gt;This challenge was not unique to us. Many teams building observability solutions on ClickHouse have encountered the same issue. Getting data into ClickHouse was straightforward, but building a UI that fully unlocked its value required significant engineering effort. For smaller teams or companies without dedicated frontend resources, ClickHouse-powered observability was often out of reach.&lt;/p&gt;

&lt;p&gt;HyperDX changed that. It provided a first-party, ClickHouse-native UI that supports log and trace exploration, correlation, and analysis at scale. Its workflows are designed with ClickHouse in mind, optimizing queries and minimizing latency. When we evaluated HyperDX prior to the acquisition, it was already clear that it addressed many of the pain points we and others had experienced. The ability to query using Lucene syntax dramatically simplifies data exploration and is often sufficient. Importantly, it still allows us to query in SQL - something which we still find essential for more complex event analysis - see “SQL for more complex analysis”.&lt;/p&gt;

&lt;p&gt;A key reason HyperDX was such a compelling fit was the schema-agnostic &lt;a href="https://clickhouse.com/blog/clickstack-a-high-performance-oss-observability-stack-on-clickhouse" rel="noopener noreferrer"&gt;approach introduced in v2.0&lt;/a&gt;. It doesn't require log tables to conform to a single, rigid structure. This flexibility is critical for a system like LogHouse, which ingests data from numerous sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It seamlessly handles the standardized, yet evolving, data format from our &lt;strong&gt;OpenTelemetry&lt;/strong&gt; pipeline.&lt;/li&gt;
&lt;li&gt;More importantly, it works out-of-the-box with the highly specialized, wide-column tables produced by &lt;strong&gt;SysEx&lt;/strong&gt; and our other custom exporters. It does this with no prior knowledge of the SysEx schemas, or complex &lt;em&gt;grok pattern&lt;/em&gt; specializations. It simply inspects the schema behind-the-scenes and adapts to work with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means our engineering teams can add new data sources with unique, optimal schemas to LogHouse without ever needing to worry about breaking or reconfiguring the user interface. By combining HyperDX's powerful UI and session replay capabilities with LogHouse's massive data repository, we have created a unified and adaptable observability experience for our engineers.&lt;/p&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%2Fdulrc3vjeewoxrquf5i5.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%2Fdulrc3vjeewoxrquf5i5.png" alt="hypdx-1.png" width="800" height="446"&gt;&lt;/a&gt;&lt;br&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%2Ftpcz3v5dfghezxqwevzf.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%2Ftpcz3v5dfghezxqwevzf.png" alt="hyperdx-2.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is worth emphasizing that Grafana still has its place in our observability stack. Our internal Grafana-based application has some distinct advantages, particularly in how it handles routing and query scoping. Users are required to specify the namespace (effectively a customer service) they intend to query. Behind the scenes, the application knows exactly where data for each service resides and can route queries directly to the appropriate ClickHouse instance within LogHouse. This minimizes unnecessary query execution across unrelated services and helps keep resource usage efficient.&lt;/p&gt;

&lt;p&gt;This is especially important in our environment, where we operate LogHouse databases across many regions. As our previous blog post described, efficiently querying across these distributed systems is critical for performance and reliability. We’re currently exploring how we might push this routing logic to ClickHouse itself, allowing HyperDX to benefit from the same optimization..so stay tuned.&lt;/p&gt;

&lt;p&gt;In addition to its routing capabilities, Grafana remains the home for many of our long-standing dashboards and alerts, particularly those built on Prometheus metrics. These remain valuable, and migrating them is not currently a priority. For example, kube_state_metrics has almost become a de facto standard for cluster health monitoring. These high-level metrics are well suited for alerting, even if they are not ideal for deep investigation. For now, they continue to serve their purpose effectively.&lt;/p&gt;

&lt;p&gt;For now, the two tools serve complementary purposes and coexist effectively within our observability stack.&lt;/p&gt;
&lt;h2&gt;
  
  
  Embracing high cardinality observability
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Store everything, aggregate nothing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The development of SysEx has brought more than just technical gains. It has driven a cultural shift in how we think about observability. By unlocking access to system tables that were previously unavailable, where only standard output logs had been captured, we have embraced a model centered on wide events and high cardinality data.&lt;/p&gt;

&lt;p&gt;Some refer to this as Observability 2.0. &lt;strong&gt;We simply call it LogHouse combined with ClickStack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This approach replaces the traditional three-pillar model with something more powerful: a centralized warehouse that can store high-cardinality telemetry from many sources. Each row contains rich context - query identifiers, pod names, version metadata, network details - without needing to pre-aggregate or discard dimensions to fit within the limits of a metric store.&lt;/p&gt;

&lt;p&gt;As engineers, we have adapted to this new model, leaving behind outdated concerns about cardinality explosions. Instead of summarizing at ingest time, we store everything as is and push aggregation to query time. This approach allows for in-depth inspection and flexible exploration without sacrificing fidelity.&lt;/p&gt;

&lt;p&gt;One pattern we have found particularly impactful is logging wide events that include timeseries attributes in place of traditional metrics. For example, here is a log line from SysEx that tracks data pushed from a source ClickHouse instance to the LogHouse cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1728437333.011701&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"caller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scrape/scrape.go:334"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pushed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"podName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c-plum-qa-31-server-zkmrfei-0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"podIP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.247.29.9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spokenName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plum-qa-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azTopoName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east1-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"srcTable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"part_log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"windowSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"insertDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00525254&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"insertLag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;300.011693981&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startGTE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1728436913&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stopLT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1728437033&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you may be asking: &lt;strong&gt;how is this different from a traditional metrics store like Prometheus?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The key difference is that we store &lt;strong&gt;every single data point&lt;/strong&gt;. We do not pre-aggregate fields like &lt;code&gt;insertDuration&lt;/code&gt;; instead, we capture and retain each value and store it together. &lt;/p&gt;

&lt;p&gt;In contrast, a system like Prometheus typically stores either a gauge per series or, more commonly, pre-aggregates values into histograms to support efficient querying. This design introduces significant limitations. For example, storing time series for all label combinations in Prometheus would lead to a cardinality explosion. In our environment, with tens of thousands of unique pod names, each label combination would require its own timeseries just to preserve query-time flexibility. Pre-aggregating with histograms helps control resource usage but comes at the cost of fidelity. It makes certain questions impossible to answer, such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which exact insert is represented by this spike in insertDuration - down to the specific instance, table, and time window?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With our approach, we avoid these trade-offs entirely. We log each event as a wide row that captures all relevant dimensions and metrics in full. This shifts aggregation and summarization to query time while preserving the ability to drill down into individual events when necessary.&lt;/p&gt;

&lt;p&gt;This model isn’t entirely new. Systems like Elasticsearch have long encouraged the ingestion of wide events and flexible document structures. The difference is that ClickHouse makes this approach operationally viable at scale. Its columnar design allows us to store high-cardinality, high-volume event data efficiently - without the runaway storage costs or query latency that traditionally limited these kinds of approaches to storing events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveraging data science tools for observability analysis
&lt;/h3&gt;

&lt;p&gt;The power of this approach is in how we can use that single event to draw many different conclusions by visualising its various characteristics, and we can always jump back to the raw logs from any given point on a chart.&lt;/p&gt;

&lt;p&gt;First, we can focus on a particular service and see its inserts line by line in series. This is the raw view upon the data.&lt;/p&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%2Fj7e2q39lcipa8ewi13wt.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%2Fj7e2q39lcipa8ewi13wt.png" alt="rawevents.png" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can visualize the insert lag for all tables for this individual instance trivially…&lt;/p&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%2F7xx731hn8bd32ntq3mwz.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%2F7xx731hn8bd32ntq3mwz.png" alt="lag.png" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We may go a layer up and visualise the insert lag for all servers in a region, which have lag &amp;gt; desired.&lt;/p&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%2F66jvlpua811uki6tl470.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%2F66jvlpua811uki6tl470.png" alt="lag-2.png" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And, because Observability is &lt;em&gt;Just another Data Problem&lt;/em&gt;, we get to borrow all of the tooling in the data science space for our observability data, so we can visualise our logs in any tool of our choice for which C&lt;a href="https://clickhouse.com/docs/integrations/data-visualization" rel="noopener noreferrer"&gt;lickHouse either integrates directly&lt;/a&gt; or &lt;a href="https://clickhouse.com/docs/integrations/language-clients" rel="noopener noreferrer"&gt;via a client library&lt;/a&gt;. For example, Plotly in a Jupyter notebook;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plotly.express&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;clickhouse_connect&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clickhouse_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SELECT
    toInt64(toFloat64(LA[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;])) AS ts,
    toInt64(LA[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;startGTE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]) AS start,
    toInt64(LA[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stopLT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]) AS stop
FROM otel.generic_logs_0
WHERE (PodName LIKE &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;loghouse-scraper-%&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND Timestamp &amp;gt;= &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2025-06-10 16:14:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND Timestamp &amp;lt;= &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2025-06-10 18:35:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND EventDate = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2025-06-10&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND Body = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pushed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND LA[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;srcTable&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;] = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text_log&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    AND LA[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;podName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;] = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;c-plum-qa-31-server-zzvuyka-0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
)
ORDER BY EventTime DESC
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_df&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Convert the 'start' and 'stop' columns from Unix timestamps to datetime objects
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_traces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bargap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://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%2Ffbv7s2nd41xh2ep1m1ic.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%2Ffbv7s2nd41xh2ep1m1ic.png" alt="scrapes.png" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The plot shows scrape time versus wall time, allowing us to inspect each event for duplication. With Plotly, I could size the width of the rectangles as the exact start/end times. The annotations highlight a window where duplicate scrapes occurred, confirming the presence of overlapping data in that range. &lt;/p&gt;
&lt;/blockquote&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%2Fsm6rtrstu3jbamml5pwu.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%2Fsm6rtrstu3jbamml5pwu.png" alt="insert-duration.png" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This plot illustrates the varying insert duration for some tables collected by the LogHouse Scraper.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While I tend to prefer Plotly, we recognize that others may favor more modern visualization libraries. Thanks to ClickHouse's broad integration support, our SREs can choose the best tools for their workflows. Whether it’s Hex, Bokeh, Evidence, or any other platform that supports SQL-driven analysis, they are free to work with the approach that suits them best.&lt;/p&gt;

&lt;p&gt;Here, we saw five views of the same event - demonstrating the flexibility we have to choose how we render at query time, using different charting tools, always with the ability to drill down into the raw line-by-line events.&lt;/p&gt;

&lt;h3&gt;
  
  
  When log search isn't enough: complex queries with SQL
&lt;/h3&gt;

&lt;p&gt;HyperDX offers a robust event search interface utilizing Lucene syntax, ideal for quick lookups and filtering. However, to answer more complex observability questions, a more expressive query language is needed. With ClickHouse as the engine behind LogHouse, we can always drop into full SQL &lt;/p&gt;

&lt;p&gt;SQL allows us to express joins, time-based operations, and transformations that would be difficult or impossible to perform in typical log query tools. One example is identifying pod termination times by correlating Kubernetes event streams. The query below uses ASOF JOIN to align Killing and Created events for the same container, calculating the time between termination and restart:&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;WITH&lt;/span&gt;
    &lt;span class="n"&gt;KE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;loghouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kube_events&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-10 01:00:00'&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="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-11 01:00:00'&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="n"&gt;Reason&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Killing'&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="n"&gt;FieldPath&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'spec.containers{c-%-server}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;CE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;loghouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kube_events&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-10 01:00:00'&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="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-11 01:00:00'&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="n"&gt;Reason&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Created'&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="n"&gt;FieldPath&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'spec.containers{c-%-server}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;killTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;createTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;createTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;killTime&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableTimeDelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;killTime&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;readableDelta&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;KE&lt;/span&gt;
&lt;span class="n"&gt;ASOF&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&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="n"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;KE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstTimestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;createTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1970-01-01 00:00:00'&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;delta&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;pre&gt;&lt;code&gt;┌─Name─────────────────────────────┬────────────killTime─┬──────────createTime─┬─delta─┬─readableDelta─────────────────────┐
│ c-emerald-tu-48-server-p0jw87g-0 │ 2025-03-10 19:01:39 │ 2025-03-10 20:15:59 │  4460 │ 1 hour, 14 minutes and 20 seconds │
│ c-azure-wb-13-server-648r93g-0   │ 2025-03-10 11:30:23 │ 2025-03-10 12:28:50 │  3507 │ 58 minutes and 27 seconds         │
│ c-azure-wb-13-server-3mjrr1g-0   │ 2025-03-10 11:30:23 │ 2025-03-10 12:28:47 │  3504 │ 58 minutes and 24 seconds         │
│ c-azure-wb-13-server-v31soea-0   │ 2025-03-10 11:30:23 │ 2025-03-10 12:28:46 │  3503 │ 58 minutes and 23 seconds         │
└──────────────────────────────────┴─────────────────────┴─────────────────────┴───────┴───────────────────────────────────┘

4 rows in set. Elapsed: 0.099 sec. Processed 17.78 million rows, 581.49 MB (180.05 million rows/s., 5.89 GB/s.)
Peak memory usage: 272.88 MiB.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sure, we could write a component to track this as a metric, but the power of ClickHouse is that we don’t need to do so. It’s sufficient to store a warehouse of wide events and derive the metric we need at query time from them. So, when a colleague asks, ‘what’s the p95 replacement time for Pods after termination is requested’, we can just find a relevant set of events instead of responding, 'let me ship a new metric ', and getting back to them with an answer after the next release goes out. &lt;/p&gt;

&lt;h3&gt;
  
  
  Expanding the data universe
&lt;/h3&gt;

&lt;p&gt;Sold on the immense value of having deep, structured telemetry in a high-performance analytics engine, we've been busy adding more data sinks to LogHouse, mainly at the request of our engineering and support team, who love using LogHouse and want all critical data to live in the warehouse. This year, we've embraced a cultural shift towards high-cardinality, wide-event-based observability as shown above. &lt;/p&gt;

&lt;p&gt;Some of our new data sources, which adhere to this wide event philosophy, include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;kubenetmon:&lt;/strong&gt; Our open-source tool for monitoring Kubernetes networking, giving us deep insights into cluster traffic. &lt;code&gt;kubenetmon&lt;/code&gt; uses Linux's conntrack system to capture L3/L4 connection data with byte/packet counts. This provides three key capabilities: forensics (time-series connection records with per-minute bandwidth), attribution (mapping connections to specific workloads and pods), and metering (cost tracking for expensive data transfer like cross-region egress). The system processes millions of connection observations per minute, helping us identify costly cross-regional downloads, track cross-AZ traffic patterns, and correlate network usage with actual costs. You can find the project at&lt;a href="https://github.com/ClickHouse/kubenetmon" rel="noopener noreferrer"&gt; https://github.com/ClickHouse/kubenetmon&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes Event Exporter:&lt;/strong&gt; We forked the popular exporter and added a native ClickHouse sink, allowing us to analyze Kubernetes API events at scale. You can find our fork &lt;a href="https://github.com/ClickHouse/kubernetes-event-exporter" rel="noopener noreferrer"&gt;here&lt;/a&gt;. This is hugely useful for understanding why things changed in K8s over time. We’re not stopping there, however! We’re already working on a plan to ingest not just the events, but the entire k8s object model into LogHouse, with snapshots at every change. This would allow us to model the full state of all clusters at any moment in time over the past six months, and step through all of the changes. Instead of just knowing "Pod X was terminated at 15:47," we’ll see the full cluster state before and after, understand dependencies, resource constraints, and the cascading effects of changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control Plane Data:&lt;/strong&gt; We collect all operational data from our Control Plane department, who had not yet onboarded into LogHouse. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real User Monitoring (RUM):&lt;/strong&gt; In a project that is still a work in progress, we collect frontend performance metrics from our users' browsers, which are pushed via a public gateway into our OTel pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Istio Access Log:&lt;/strong&gt; We ingest HTTP-level traffic data from our Istio service mesh, capturing request/response patterns, latencies, and routing decisions. Combined with ClickHouse's system.query_log and kubenetmon's network flows, this creates a powerful tri-dimensional correlation capability. When network usage spikes occur, our support team can trace the complete story: which specific SQL queries were executing, what HTTP requests triggered them, and the exact packet flow patterns. This cross-layer visibility transforms debugging from guesswork into precise root cause analysis - if we see unusual egress traffic, we can immediately identify whether it's from expensive cross-region queries, backup operations, or unexpected replication, making troubleshooting incredibly efficient for the support team.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next and the road ahead
&lt;/h2&gt;

&lt;p&gt;It’s been an incredible year of growth for LogHouse. By moving beyond a one-size-fits-all approach and embracing specialized, highly efficient tooling, we’ve scaled our observability platform to remarkable new heights while significantly enhancing our cost performance. Integrating HyperDX is a key part of that evolution, providing a flexible and powerful user experience on top of our petabyte-scale data warehouse. We're excited to see what the next year brings as we continue to build on this strong foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Toward zero-impact scraping
&lt;/h3&gt;

&lt;p&gt;While SysEx is designed to be efficient and resource-conscious, customers occasionally notice our scrape queries in their logs and metrics. These queries are &lt;strong&gt;tightly constrained with strict memory limits&lt;/strong&gt;, but when they error (as they sometimes do) it can create concern. Although the actual resource impact is minimal, we recognize that even lightweight queries can create noise or confusion in sensitive environments.&lt;/p&gt;

&lt;p&gt;To address this, we’re exploring what we call &lt;strong&gt;zero-impact scraping&lt;/strong&gt; - the next evolution of SysEx. The goal is to eliminate all in-cluster query execution by entirely decoupling scraping from the live system. One promising direction involves leveraging &lt;strong&gt;&lt;a href="https://clickhouse.com/docs/operations/storing-data#s3-plain-rewritable-storage" rel="noopener noreferrer"&gt;plain rewritable disks on S3&lt;/a&gt;&lt;/strong&gt;, where ClickHouse already writes its service logs. In this model, a pool of SysEx workers would mount these disk-based log tables directly, bypassing the need to query the running ClickHouse instance. This design would deliver all the benefits of our current system - native format, high fidelity, minimal transformation - while removing even the perception of operational impact.&lt;/p&gt;

&lt;p&gt;OpenTelemetry remains a critical component of our platform, particularly for early-stage data capture before service tables are available. This is especially useful during crash loops, where structured logs may be unavailable. However, if our zero-impact scraping approach proves successful, it could reduce our reliance on OTel even further by providing a high-fidelity, low-disruption path for log ingestion throughout the lifecycle of a cluster.&lt;/p&gt;

&lt;p&gt;This effort is still in progress, and we’ll share more once we’ve validated the approach in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating to JSON
&lt;/h3&gt;

&lt;p&gt;The JSON type has been available in ClickHouse for some time and &lt;a href="https://clickhouse.com/blog/clickhouse-release-25-03" rel="noopener noreferrer"&gt;recently reached GA in version 25.3&lt;/a&gt;. It offers a flexible and efficient way to store semi-structured data, dynamically creating columns with appropriate types as new fields appear. It even supports &lt;a href="https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse" rel="noopener noreferrer"&gt;fields with multiple types&lt;/a&gt; and gracefully handles schema explosion.&lt;/p&gt;

&lt;p&gt;Despite these advantages, we’re still evaluating how well JSON fits common observability access patterns at scale. For example, querying a string across an entire JSON blob can effectively involve scanning thousands of columns. There are workarounds - such as also storing a raw string version of the JSON alongside the structured data - but we’re still developing best practices in this area. &lt;/p&gt;

&lt;p&gt;Culturally, we have also come to recognize the practical limits of the Map type, which has served us well. Most of our log and resource attributes are small and stable enough that the Map continues to be the right fit. We have found that single-level JSON logs are often all you need, and for exceptions, tools like HyperDX automatically translate map access into &lt;a href="https://clickhouse.com/docs/sql-reference/functions/json-functions#jsonextract" rel="noopener noreferrer"&gt;JSONExtract&lt;/a&gt; functions. While we plan to adopt JSON more broadly, this is still a work in progress. Expect us to share more in a future update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Over the past year, LogHouse has evolved from an ambitious logging system into a foundational observability platform powering everything from performance analysis to real-time debugging across ClickHouse Cloud. What began as a cost-saving measure has become a catalyst for both cultural and technical transformation, shifting us toward high-fidelity, wide-event telemetry at massive scale. By combining specialized tools like SysEx with general-purpose frameworks like OpenTelemetry, and layering on flexible interfaces like HyperDX, we have built a system that not only keeps up with our growth but also unlocks entirely new workflows. The journey is far from over, but the lessons from scaling to 100PB and 500 trillion rows continue to shape how we think about observability as a core data problem we are solving at warehouse scale.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>database</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
