All posts

5 min of ClickHouse: What ALTER ... DELETE Really Costs on a Billion-Row Table

Why ALTER TABLE ... DELETE rewrites whole parts instead of removing rows, how to track progress in system.mutations, and cheaper alternatives.

Fourth in the series. ALTER TABLE ... DELETE (and UPDATE) reads like a normal SQL statement, but on a MergeTree table it’s nothing like a Postgres DELETE. Understanding what it actually does is the difference between a five-minute cleanup and an hours-long background job that starves your merges.

Why it’s not a real delete

MergeTree parts are immutable — ClickHouse never edits a row in place. An ALTER ... DELETE (or UPDATE) is a mutation: ClickHouse rewrites every part that contains at least one matching row, in the background, part by part. If your WHERE clause matches one row in a 50GB part, ClickHouse rewrites the entire 50GB part to produce a new one without that row. Rows aren’t deleted — parts are replaced.

That’s why a mutation on a billion-row table can take hours: the cost scales with the size of every part that contains a match, not the number of rows deleted.

Watch it happen

SELECT
    database || '.' || table AS table,
    mutation_id,
    command,
    create_time,
    now() - create_time AS elapsed,
    parts_to_do,
    is_done,
    latest_fail_reason
FROM system.mutations
WHERE is_done = 0
ORDER BY create_time DESC

What it costs beyond time

Cheaper alternatives

If a mutation is stuck

KILL MUTATION WHERE mutation_id = '<mutation_id>'

Killing it stops further parts from being rewritten, but any parts it already finished stay rewritten — it’s not atomic. Fix the underlying cause (usually a schema mismatch in the WHERE/SET expression, or a disk-space issue) before re-submitting.

How chmonitor surfaces this

Mutations lists every pending and completed mutation with parts_to_do, is_done, and failure reason, flags mutations stuck past a threshold, and has a one-click kill action for the stuck ones — the exact query and action above, no SQL required.

chmonitor does this for you

chmonitor tracks every mutation from submission to completion and flags stuck ones automatically, so a rewrite job doesn’t silently starve your merges for hours.

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.