clickhouse 'Memory limit (for query) exceeded': which part of the query blew up
Shows how to find which part of a ClickHouse query blew the memory limit when 'Memory limit (for query) exceeded' fires. Use when a ClickHouse query dies on memory and you need the offending stage. Trigger: the exact memory-limit error from ClickHouse.
clickhouse 'Memory limit (for query) exceeded': which part of the query blew up
TL;DR
Check the query log for the failed query's peak memory, then EXPLAIN the pipeline to find the memory-hungry stage - usually a GROUP BY with huge cardinality, a big JOIN, or an ORDER BY over the full result. Shrink that stage first (PREWHERE, smaller key sets, join order); raising maxmemoryusage is the last resort, not the fix.
The error
Memory limit (for query) exceededSteps
- Find the failed query and its peak memory in the query log:
SELECT query, memory_usage, peak_memory_usage, query_duration_ms
FROM system.query_log
WHERE type = 'ExceptionBeforeStart' OR exception LIKE '%Memory limit%'
ORDER BY event_time DESC
LIMIT 5;Expected: the failed query text plus how far past the limit its peak went.
- EXPLAIN the query's pipeline to see the stages:
EXPLAIN PIPELINE [your query]Expected: a stage list where you can spot the aggregation, join, or sort that materializes the most data.
- Check the cardinality of your GROUP BY keys. A GROUP BY over a high-cardinality key (user IDs, request IDs) builds a hash table with one entry per group:
SELECT uniqExact([group key]) FROM [table] WHERE [your filters]Expected: a cardinality number; millions of groups means the aggregation state is the memory hog.
Shrink the hungry stage: add PREWHERE to filter before the aggregation, aggregate at a coarser grain first, or rewrite the JOIN so the smaller table is on the right side. Re-run and watch peakmemoryusage fall.
Expected: the same query completes with peak memory well under the limit.
- Only if the query is legitimately large, raise the limit for that query alone rather than globally:
SET max_memory_usage = 20000000000;
-- then run the query in this sessionExpected: the query completes; the global limit still protects the server from runaway queries.
Use this when
- ClickHouse returns the exact
Memory limit (for query) exceedederror. - You need to attribute the memory to a specific stage of the query.
- A previously working query starts failing as data grows.
Not for this skill when
- The error is
Memory limit (total) exceeded(that is server-wide pressure, not one query; check concurrent load). - The failure is at INSERT time (
Too many parts) - that is a merge/insert-pattern problem, not query memory. - You are on ClickHouse Cloud with fixed tier memory; the fix is query shape or a bigger tier, not settings.
Variant phrasings
clickhouse memory limit exceeded which part of query
The query-log plus EXPLAIN PIPELINE workflow above.
clickhouse query dies with memory limit on group by
High-cardinality GROUP BY is the classic cause; step 3 confirms it.
how to find memory hungry stage clickhouse
system.querylog peakmemory_usage plus EXPLAIN PIPELINE.
Why it happens
ClickHouse executes queries as a pipeline of stages, and stages like aggregation, joins, and sorts must hold their working set in memory - a GROUP BY keeps one state per group, a JOIN keeps the right-hand table. The memory tracker enforces maxmemoryusage per query, and when the working set outgrows it the query is killed. Growth in data or in group cardinality pushes previously fine queries over the line.
Edge cases
max_bytes_before_external_group_byandmax_bytes_before_external_sortlet those two operators spill to disk instead of dying; useful as a safety valve, slower than fixing the query.- Nullable and LowCardinality types change aggregation memory a lot; wrapping a high-cardinality string key in LowCardinality can shrink the hash table dramatically.
- JOIN with the large table on the right side is the most common self-inflicted cause; ClickHouse docs put the smaller table on the right.
- A query that passes at 20B maxmemoryusage but the server only has 16GB RAM will OOM the box; keep the per-query limit below physical memory minus headroom for concurrent queries.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_pbLDkvXJmWGrQZnKrbKtPA