All posts
Reading ClickHouse® Database Query Flame Graphs

Reading ClickHouse® Database Query Flame Graphs

May 14, 20264 min readSanjeev Kumar G
Share:

System tables tell you that a query was slow. A flame graph tells you why. Once I understood how to read one, the hardest part of query tuning, finding the actual bottleneck, went from an afternoon of guessing to a couple of minutes of looking. Here is how I read them for ClickHouse®.

What a flame graph shows

A flame graph is a picture of where time went inside a single query. The width of each bar is how much time was spent in that part of the execution. Wide bars are where your query spent its time. Narrow bars are cheap. You read it by scanning for the widest bars, because that is where the opportunity is.

In the CHOps query profiler you run a query, and it builds the flame graph from ClickHouse®'s own profiling data. You are not guessing from query_duration_ms. You are seeing the breakdown: how much went to reading data, how much to filtering, how much to aggregation, how much to sorting.

The pattern I look for first

The most common thing a flame graph reveals is that a query spends almost all its time reading data it did not need. You see a huge wide bar for the read stage and a thin sliver for the actual aggregation. That is the signature of a primary key that is not being used. The fix is not to make the aggregation faster, it is to read less data in the first place, usually by filtering on the primary key or adding a projection.

The second pattern is the opposite: reads are cheap but a sort or a GROUP BY with high cardinality dominates. There the fix is different. Maybe you can aggregate at a coarser granularity, or use an approximate function like uniqHLL12 instead of uniqExact, or avoid the global sort entirely.

The flame graph tells you which of these you are dealing with, which matters, because optimizing the wrong stage wastes your time.

A worked example

Say a dashboard query takes four seconds. I open it in the profiler and the flame graph shows one enormous bar: reading from the events table, scanning 800 million rows. The aggregation on top is tiny. So the problem is clear, the query is reading the whole table.

I check the filter:

SELECT count()
FROM analytics.events
WHERE toYYYYMM(event_time) = 202605;

The issue jumps out. The table is partitioned by event_date, but the filter wraps event_time in a function, so ClickHouse® cannot use the partition to skip data. Rewriting it to filter the raw column directly lets ClickHouse® prune partitions:

SELECT count()
FROM analytics.events
WHERE event_date >= '2026-05-01'
  AND event_date < '2026-06-01';

Same result, but now the read bar in the flame graph shrinks from 800 million rows to a few million, and the query returns in well under a second. The flame graph is what made the cause obvious instead of something I had to deduce.

Why the visual matters

You can get at the same information through EXPLAIN and the system tables, and I covered that approach in debugging slow queries with system tables. But reading a flame graph is faster for the common cases because the bottleneck is literally the widest thing on the screen. There is no mental arithmetic. You look, you see the wide bar, you know where to focus.

For anyone learning ClickHouse® performance, I would actually recommend starting with the flame graph. It builds the right intuition: queries are slow because of what they do too much of, and the flame graph shows you that part directly. The query profiler feature page has more on the resource timelines and the query inspector that sit alongside it.

Share: