Dashboard

What goes in a ClickHouse monitoring dashboard in Metabase?

A ClickHouse monitoring dashboard tracks queries per second and duration percentiles alongside the storage-engine numbers that actually decide them — active parts, merge backlog, and replication delay. Metabase connects to ClickHouse natively, so every card is plain SQL over the cluster's own system tables — no exporter or metrics agent in between.

For: the team running ClickHouse as a serving layer, and whoever gets paged when p99 moves. Grain: one row per query, plus point-in-time engine state. Source: system.query_log, system.parts, system.merges, system.replicas (query_log TTL is typically 30 days — roll up nightly for more).

What does a ClickHouse monitoring dashboard look like?

Here’s the layout this guide builds. Cluster-level numbers sit at the top; the middle section is what clients experience — throughput, latency percentiles, read volume, and failures; the bottom section is the engine underneath, where parts, merges, and replication explain why the middle section moved.

ClickHouse monitoring dashboard in Metabase showing queries per second, duration percentiles, failed queries, active parts, merge backlog, and replication delay.

An example ClickHouse monitoring dashboard in Metabase, built from the cluster’s system tables. Figures are illustrative.

Which cards belong on a ClickHouse monitoring dashboard?

Eight cards, in two halves: what clients see, and what the storage engine is doing. The second half is the one most dashboards omit and the one that explains almost every latency regression.

  • Queries per second by kind — SELECT vs. INSERT, by hour (area)
  • Query duration — p50, p95, p99 by day (line)
  • Data read from disk and rows read per second (combo)
  • Failed queries by exception — memory limit, too many parts, timeout (donut)
  • Active parts against parts_to_throw_insert on the busiest partition (line with goal)
  • Merge backlog — merges running and mutations queued (bar)
  • Replication delay by replica (row)
  • Top queries by peak memory, with calls, p99, and rows read (table)

What data does the dashboard need?

  • system.query_logtype, query_duration_ms, read_rows, read_bytes, memory_usage, exception_code, is_initial_query, and the normalized query hash.
  • system.parts for active part counts per table and partition, plus data_compressed_bytes and data_uncompressed_bytes for the compression ratio.
  • system.merges and system.mutations for merges in flight, progress, and anything stuck in the queue.
  • system.replicasabsolute_delay, queue_size, inserts_in_queue, is_readonly — for the replication card.
  • system.asynchronous_metrics, system.metrics, and system.errors for host-level memory, disk space, cache hit counters, and error totals.
  • Your configured parts_to_delay_insert and parts_to_throw_insert values, so the parts card has a real ceiling rather than a guessed one.

How do you build it?

  1. Connect ClickHouse to Metabase with a read-only user that can see the system database, and give it a settings profile with modest max_memory_usage so a monitoring query can never be the thing that takes the cluster down.
  2. Wrap cluster-wide reads in clusterAllReplicas(...) and always filter WHERE is_initial_query, or distributed queries get counted once per shard.
  3. Schedule a nightly rollup of query_log into your own MergeTree table — the log has a TTL, and the trend cards need more history than it keeps.
  4. Build the latency and parts cards first, side by side: they are the pair that turns “the dashboard is slow” into a specific fix. Then add merges, replication, and the memory table.
  5. Add filters for host or shard, table, and date range, and alert when active parts cross a fraction of parts_to_throw_insert or a replica’s delay exceeds its budget.

Example card SQL

Queries, failures, duration percentiles, and read volume by hour PostgreSQL
SELECT
  toStartOfHour(event_time)                        AS hour,
  countIf(type = 'QueryFinish')                    AS queries,
  countIf(type IN ('ExceptionBeforeStart',
                   'ExceptionWhileProcessing'))    AS failed,
  round(quantile(0.50)(query_duration_ms))         AS p50_ms,
  round(quantile(0.95)(query_duration_ms))         AS p95_ms,
  round(quantile(0.99)(query_duration_ms))         AS p99_ms,
  round(sum(read_rows) / 3600)                     AS rows_per_sec,
  formatReadableSize(sum(read_bytes))              AS bytes_read,
  formatReadableSize(max(memory_usage))            AS peak_memory
FROM clusterAllReplicas('default', system.query_log)
WHERE event_time >= now() - INTERVAL 24 HOUR
AND is_initial_query
AND query_kind = 'Select'
GROUP BY hour
ORDER BY hour;

Metrics

Integrations

Dashboards

FAQ

What is a ClickHouse monitoring dashboard?
A ClickHouse monitoring dashboard tracks whether the cluster is still serving reads at the latency your product promises, and whether the storage engine underneath is keeping up. Throughput and duration percentiles cover the first; active parts, merge backlog, and replication delay cover the second — and in ClickHouse the second is usually what breaks the first. Metabase connects to ClickHouse natively, so every card is ordinary SQL against the cluster's own system database. No exporter, no separate time-series store.
Which system tables should the cards read?
system.query_log carries the query half: one row per query with type, query_duration_ms, read_rows, read_bytes, memory_usage, exception_code, and the query text. The storage half comes from system.parts (active parts, rows and bytes per part, compressed vs. uncompressed size), system.merges and system.mutations (what is merging right now and what is queued), and system.replicas (absolute_delay, queue_size, inserts_in_queue). system.asynchronous_metrics, system.metrics, and system.events fill in host-level counters — memory, cache hits, disk space — and system.errors gives a running count per error code.
Why do active parts deserve their own card?
Because they are a hard limit, not a soft one. Every insert creates a new part, background merges combine them, and if inserts outrun merges the count climbs. At parts_to_delay_insert (default 1,000) ClickHouse starts slowing inserts down; at parts_to_throw_insert (default 3,000) it rejects them with TOO_MANY_PARTS. Charting active parts on the busiest partition against your configured ceiling turns a sudden outage into two weeks of warning. The usual causes are small, frequent inserts (batch them, or use async inserts) and too many partitions — monthly partitioning is right far more often than daily.
Why does read latency rise when merges back up?
A SELECT has to read every active part that matches the query's partitions and primary-key range, so more parts means more files, more marks, and more merge work competing for the same disk and CPU. That is why the merge-backlog card sits beside the latency card in this dashboard: in the example p99 doubles from 190ms to 412ms while merges in flight go from 6 to 18, with no increase in query volume at all. If you see latency climbing and throughput flat, look at parts and merges before you look at the queries.
How do I query system.query_log on a multi-node cluster?
Two rules. Wrap the table in clusterAllReplicas('your_cluster', system.query_log), because each node only logs its own queries, and filter WHERE is_initial_query so a distributed query is counted once rather than once per shard. Skipping the second is the classic mistake: query counts inflate by roughly the shard count and duration percentiles get dragged down by the fast per-shard sub-queries. Note also that query_log is written asynchronously — rows appear after flush_interval_milliseconds, 7.5 seconds by default — so the “last minute” is always slightly incomplete.
Does keeping query_log around cost anything?
Some disk, which is why system.query_log ships with a TTL — typically 30 days — and older rows disappear. If you want quarter-over-quarter trends, schedule a nightly rollup into your own MergeTree table with the aggregates the dashboard needs, and point the trend cards at that. It also keeps the dashboard cheap: a card that re-scans raw query_log on every refresh is itself a query in query_log, and on a busy cluster that table is not small.
How is this different from a real-time analytics dashboard?
Subject. A real-time analytics dashboard uses ClickHouse to watch a live business — events, sessions, streams arriving now. This one watches ClickHouse itself: parts, merges, replicas, exceptions, latency percentiles. Teams that run ClickHouse as the serving layer under a product usually keep both, plus an application monitoring dashboard for the service in front of it. For the analytical-warehouse equivalents, see BigQuery, Snowflake, and the engine-agnostic data warehouse dashboard.