Schedule icon
Query icon
OutputValues icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Fail icon

ClickHouse Expired Partition Drop Guard

Detect ClickHouse partitions past a retention window in system.parts, drop them behind a dry-run and approval gate, then re-read to confirm.

Categories
DataInfrastructure

ClickHouse MergeTree tables keep old partitions on disk when no TTL is set or when a TTL exists but its DELETE was never materialized by a merge. Storage then grows past the retention policy while every query still pays to skip the dead parts. This guard finds those partitions and drops them under a gate, then proves the drop landed.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. dry_run is true and auto_remediate is false, so a plain run reports and never drops.
  • A drop happens only when auto_remediate is true and dry_run is false. It then waits for a human to resume the Pause with approved set true. An unanswered gate reads as not approved.
  • Expiry is read from the newest row timestamp in system.parts, so a partition whose age cannot be read (a non temporal partition key) is skipped and never dropped.
  • Each drop is isolated and the loop runs one statement at a time, so one blocked drop does not stop the others and parallel DDL never races on the same table.

How it works

  1. detect (io.kestra.plugin.jdbc.clickhouse.Query) reads system.parts for the table, groups active parts by partition and keeps partitions whose newest row is older than now() - retention_days. It reads the newest row as greatest(max(max_time), toDateTime(max(max_date))), correct for toYYYYMM, toDate and toYYYYMMDD partition keys. That same value must also be above the epoch floor, so a non temporal key is never selected.
  2. summarize (io.kestra.plugin.core.output.OutputValues) records the expired partition id list once for the logs, the alert and the failure message.
  3. guard (io.kestra.plugin.core.flow.If) branches on the expired count. With none found, clean logs and the run ends.
  4. findings_alert (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the breach on every detection, in dry run and in remediate mode.
  5. mode (io.kestra.plugin.core.flow.If) enters the remediation branch only when auto_remediate is true and dry_run is false. Otherwise report_only lists what would be dropped and stops.
  6. approval_gate (io.kestra.plugin.core.flow.Pause) holds the run until a human resumes it with approved. apply runs the drop only when approved is true.
  7. drop_expired_partitions (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs ALTER TABLE ... DROP PARTITION ID once per expired partition.
  8. reverify re-reads system.parts with the same window. verify_result then fails the run through io.kestra.plugin.core.execution.Fail if any expired partition is still present.

What you get

  • A retention check that reads the real data age from system.parts, not the partition key format, so it works across date and datetime partition keys.
  • A drop that is safe to leave scheduled. It reports until an operator turns on remediation and approves each run.
  • A real verification step. The run passes only when the second read of system.parts shows zero expired partitions.
  • Slack alerts on the findings and on any failure.

Who it's for

  • Data platform engineers who run ClickHouse and need storage held to a retention policy without hand-written SQL.
  • DBAs cleaning up tables that have no TTL or whose TTL was never materialized.
  • Teams evaluating the ClickHouse plugin who want an example that reads a system table and runs gated DDL.

Why orchestrate this with Kestra

ClickHouse has no scheduler, no approval step and no way to prove a maintenance command took effect. Kestra adds the missing pieces. It schedules the sweep, gates the destructive statement behind dry-run and approval, re-reads system.parts to turn a hopeful drop into a verified one, applies the same connection settings to every task and posts a Slack alert the moment a step fails.

Tested end to end

Run on Kestra 2.0.5 against a ClickHouse 24.8 server with a toYYYYMM(ts) table seeded with three 2024 partitions and two recent ones.

  • Detector can fail: a 3650 day window selected 0 partitions, a 90 day window selected 3 on the same table.
  • Dry run at 90 days detected 3, alerted and left all 3 in place (confirmed with clickhouse-client).
  • Remediate with approval dropped all 3 and the re-read reported 0 remaining (3 before, 0 after in system.parts).
  • The approval gate held. A run resumed with approved false dropped nothing.
  • A read-only user turned every drop into a no-op, the re-read found 3 still expired and the run failed and alerted.
  • An unreachable host failed the detect step and fired the Slack alert.

Prerequisites

A ClickHouse server reachable over the JDBC HTTP interface and a user that can read system.parts and run ALTER TABLE ... DROP PARTITION on the target table.

Secrets

  • CLICKHOUSE_USERNAME: ClickHouse user, read when use_secret_auth is true.
  • CLICKHOUSE_PASSWORD: password for that user.
  • SLACK_WEBHOOK_URL: used by findings_alert and the errors block.
  • In Kestra OSS, provide each secret as a base64-encoded environment variable prefixed SECRET_.

Inputs

Name Type Default Description
clickhouse_url STRING jdbc:clickhouse://localhost:8123/default ClickHouse JDBC URL.
use_secret_auth BOOL true Read credentials from secrets. Set false only for inline test credentials.
database STRING default Database holding the table.
table STRING events MergeTree table to guard.
retention_days INT 90 A partition is expired when its newest row is older than this.
dry_run BOOL true Report without dropping.
auto_remediate BOOL false Drop behind the approval gate when dry_run is false.

Quick start

  1. Set CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD and SLACK_WEBHOOK_URL as namespace secrets.
  2. Import this flow.
  3. Run it with database and table pointed at a partitioned MergeTree table. The default dry run reports the expired partitions.
  4. To drop, run again with dry_run false and auto_remediate true, then resume the paused execution with approved true.

How to extend

  • Loop over several tables from one flow by moving the detect and drop steps into a table loop.
  • Replace DROP PARTITION with MOVE PARTITION ... TO DISK to tier old data to object storage instead of deleting it.
  • Add a second window that only alerts, so near-expiry partitions are visible before they are dropped.
  • Fail the run as soon as detection finds anything, for a read-only drift check with no remediation.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.