All posts

5 min of ClickHouse: Finding Your 10 Slowest Queries from system.query_log

The exact system.query_log query to rank your slowest ClickHouse queries, what each column means, and what to check next.

Second in the series. Every ClickHouse instance already has the answer to “what’s slow?” sitting in system.query_log — you just have to ask it the right way. No APM agent, no extra instrumentation, it’s on by default.

Want the full walkthrough? This is the 5-minute version — for version differences and continuous-monitoring options, see Finding your slowest ClickHouse queries with system.query_log: a complete walkthrough.

Prerequisites

The query

SELECT
    query_id,
    query_start_time,
    query_duration_ms / 1000 AS query_duration_s,
    user,
    read_rows,
    formatReadableQuantity(read_rows) AS readable_read_rows,
    formatReadableSize(read_bytes) AS readable_read_bytes,
    formatReadableSize(memory_usage) AS readable_memory_usage,
    replace(substr(query, 1, 200), '\n', ' ') AS query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query_duration_ms >= 5000
  AND event_time > now() - INTERVAL 24 HOUR
ORDER BY query_duration_ms DESC
LIMIT 10

That’s the same query chmonitor’s Slow Queries page runs, minus the app’s readable-size and background-bar formatting. Swap the 5000 (5 seconds) and 24 HOUR window for whatever you’re chasing.

Reading the columns

Group by query shape, not query_id

Ten slow executions often boil down to one or two slow query shapes run repeatedly with different literals. Group by normalized_query_hash to see the pattern instead of ten near-duplicate rows:

SELECT
    normalized_query_hash,
    count() AS executions,
    sum(query_duration_ms) / 1000 AS total_duration_s,
    avg(query_duration_ms) / 1000 AS avg_duration_s,
    quantile(0.99)(query_duration_ms) / 1000 AS p99_duration_s,
    replace(substr(argMax(query, query_duration_ms), 1, 200), '\n', ' ') AS sample_query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND event_time > now() - INTERVAL 24 HOUR
GROUP BY normalized_query_hash
ORDER BY total_duration_s DESC
LIMIT 10

total_duration_s ranks by aggregate cost to the cluster — the query that runs 500 times a day at 200ms each often costs more than the one slow 10-second report someone runs once. A wide gap between p99_duration_s and avg_duration_s on the same shape usually means the plan is fine but is occasionally starved (lock contention, a cold cache, or resource contention from a concurrent big query) rather than fundamentally slow.

What to check next

Once you have the offending query, don’t guess — run EXPLAIN indexes = 1 <query> and look at Granules: N/M. If N is close to M, the primary key isn’t pruning anything and you’re doing a full scan; that’s a schema or WHERE-clause problem, not a “ClickHouse is slow” problem.

How chmonitor surfaces this

Slow Queries runs exactly this query with a duration/time-window filter, and each row has a one-click “Explain query” action so you don’t have to copy the SQL out to a separate client. Expensive Queries does the normalized_query_hash grouping above, ranked by total CPU time across every execution.

chmonitor does this for you

chmonitor runs this query continuously, ranks results live, and lets the AI agent explain why a specific query is slow — not just that it is.

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.