All posts

5 min of ClickHouse: Diagnosing 'Too Many Parts' from system.parts

Why ClickHouse throws Too many parts, the exact system.parts query to find the offending table, and how to fix it without guessing.

Five minutes, one real diagnostic query, no fluff. First in the series: the error almost every ClickHouse operator hits eventually — Too many parts. It looks like an insert failure, but it’s actually a merge-throughput problem wearing an insert’s clothes.

The symptom

Inserts start failing (or slowing to a crawl) with an exception whose message mentions “too many parts” and that merges can’t keep up with inserts. It’s not random — ClickHouse is protecting itself. Every INSERT creates at least one new part, and every SELECT has to open and merge-read every active part in the query’s range. Merges run in the background to consolidate small parts into bigger ones; if inserts create parts faster than the background pool can merge them, the part count climbs until ClickHouse throttles, then rejects, inserts.

Three MergeTree settings enforce this, in escalating order:

Setting Default What happens
parts_to_delay_insert 150 Inserts start sleeping (artificial backpressure) once a partition crosses this many active parts
parts_to_throw_insert 3000 Inserts fail outright with TOO_MANY_PARTS
max_parts_in_total 100000 Server-wide ceiling across all partitions of a table

Find the offending table

Don’t guess which table is causing it — count active parts per table and sort descending:

SELECT
    database,
    table,
    count() AS part_count,
    uniqExact(partition) AS partition_count,
    round(part_count / partition_count, 1) AS avg_parts_per_partition,
    formatReadableSize(sum(bytes_on_disk)) AS total_size
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY part_count DESC
LIMIT 20

A healthy MergeTree table sits well under 150 active parts per partition most of the time — background merges keep consolidating small parts into larger ones. If avg_parts_per_partition is climbing toward the 150/3000 thresholds above, that table is the one to fix.

Then drill into which partition specifically is accumulating parts:

SELECT
    partition,
    count() AS part_count,
    min(modification_time) AS oldest_part,
    max(modification_time) AS newest_part,
    formatReadableSize(sum(bytes_on_disk)) AS total_size
FROM system.parts
WHERE active AND database = {database:String} AND table = {table:String}
GROUP BY partition
ORDER BY part_count DESC
LIMIT 20

Common causes

Too many small inserts. Each INSERT statement — even one with a single row — creates a new part. High-frequency, low-batch-size inserts (a common mistake when streaming events row-by-row instead of batching) are the single biggest cause. Batch inserts client-side or through async_insert before they hit the table.

Merges falling behind. Check whether merges are actually running and how fast:

SELECT count() AS active_merges,
       sum(rows_read) AS rows_being_merged
FROM system.merges

If this is consistently near zero while part counts climb, merges aren’t keeping pace — see the next post in this series for reading system.merges in depth. A single overloaded disk, a background_pool_size that’s too small for the insert rate, or CPU contention from other queries can all starve the merge scheduler.

Partition key too granular. Daily or hourly partitioning on a high-volume table multiplies the number of partitions ClickHouse has to track and merge independently — merges only happen within a partition, never across. See partition key mistakes that quietly kill performance for the full picture.

Fix

How chmonitor surfaces this

The Tables page’s Tables Overview lists parts_count per table with a link straight into the per-table Part Info view — the same system.parts grouping shown above, without writing SQL. The AI agent’s get_table_parts tool runs the same diagnostic on request (“why does events have so many parts?”).

chmonitor does this for you

chmonitor runs this diagnostic continuously and flags tables trending toward the 150/3000 thresholds before they start rejecting inserts — no dashboards to build by hand.

docker run -d --name chmonitor -p 3000:3000 \
  -e CLICKHOUSE_HOST=https://clickhouse.example.com:8443 \
  -e CLICKHOUSE_USER=default \
  -e CLICKHOUSE_PASSWORD=change-me \
  ghcr.io/chmonitor/chmonitor:latest

Or skip setup and try the live demo.