chmonitor
Guides

ClickHouse query optimization: the definitive guide

Six root causes of slow ClickHouse queries — partition keys, granularity, PREWHERE vs WHERE, projections vs materialized views, skip indices, and external GROUP BY — with diagnostic SQL for each.

Most "ClickHouse is slow" problems trace back to one of six things: a bad partition key, the wrong partition granularity, a filter that isn't pushed into PREWHERE, a missing skip index, the wrong choice between a projection and a materialized view, or a GROUP BY that runs out of memory. This page is the map — each section below links to a full guide with diagnostic SQL you can run against system.parts, system.query_log, and system.data_skipping_indices right now.

chmonitor runs this diagnosis for you

Every query below is also what chmonitor's AI agent runs automatically. Ask it "why is my ClickHouse cluster slow?" and it works through list_slow_query_patternsexplain_queryget_optimization_recommendations, then proposes ranked skip-index, projection, partition-key, and PREWHERE fixes as DDL you review and run yourself — it never applies anything on its own. See AI Agent capabilities.

The six areas

Where to start

Work top to bottom — each step rules out a cheaper explanation before you reach for a more invasive fix.

Check part and partition health first

A bad partition key or the wrong granularity causes symptoms everywhere else — merge backlogs, slow inserts, and OOM on GROUP BY all get worse when a table has thousands of tiny parts. Rule this out before tuning anything else.

SELECT
    database,
    table,
    count() AS active_parts,
    uniqExact(partition) AS partitions,
    formatReadableSize(sum(data_compressed_bytes)) AS compressed_size
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY active_parts DESC
LIMIT 20;

More than a few hundred active parts on one table, or fewer than ~10 MB compressed per part, is a partitioning problem — see the two guides above.

Check whether the filter is doing its job

For a specific slow query, run EXPLAIN indexes = 1 <query> and compare Granules: N/M. If N is close to M, the query is scanning almost everything — the filter isn't pruning. That's a PREWHERE or skip index problem.

Decide how to accelerate the repeated shape

If the same query shape (same GROUP BY / WHERE pattern) runs often, decide between a projection or a materialized view to pre-compute it instead of re-scanning raw data every time.

Tune memory only after the query is actually necessary

If the query still needs to scan a lot of data by nature (a genuinely high-cardinality aggregation), see external GROUP BY for spill settings — but only after the steps above, since fixing the index or partition problem is usually cheaper than spilling to disk.

On this page