Skip to main content
Diagnosing a slow query starts with evidence from its query history. This guide shows you how to use system.query_log to find recurring slow-query patterns, choose a representative run, and review its resource usage. You will then use EXPLAIN to inspect the query plan and form a hypothesis about the bottleneck before changing the query or schema.

Before you begin

The examples in this guide use the nyc_taxi.trips_small_inferred table. To run them as written, create and load the table if you have not already done so:
The source Parquet file is approximately 5.8 GB. Loading it can take several minutes, depending on your network and available resources.
To reproduce the query-log results in this guide, run all three example workload queries at least twice after loading the dataset. Then flush the query log so that the completed runs are available to the examples below:
If you cannot run SYSTEM FLUSH LOGS, wait for the query log to flush automatically, then retry the first lookup. When diagnosing your own workload, ensure that system.query_log contains completed runs from the time range you intend to inspect.

How it works

By default, ClickHouse records information about completed queries in the system.query_log table. Each record can include the query duration, the number of rows read, CPU and memory usage, and filesystem cache activity. These measurements help you identify slow query patterns and understand how they use resources. After choosing a representative run, you can inspect its query plan to investigate where the query might be spending time. On a cluster, query-log data remains local to each node. The examples in this guide use clusterAllReplicas to query every replica and merge to include the current system.query_log table and any versioned query_log_N tables retained after system-table schema changes. Each query-log example includes tabs for clustered and single-node deployments. ClickHouse Cloud provides the default cluster used in the cluster examples. In a self-managed deployment, replace default with a cluster listed in system.clusters.
The examples set skip_unavailable_shards so that a temporarily unavailable replica does not cause the diagnostic query to fail. This is particularly useful during autoscaling. Records from a skipped replica are not included, so the results may be incomplete.

Diagnose a slow query

With completed runs in the query log, work through these three steps in order. You will identify a recurring slow-query pattern, choose a representative run, and inspect the query’s execution plan.
1

Identify candidate queries

Start by grouping completed initial queries by normalized_query_hash. This separates query patterns that recur from individual slow executions. The following query ranks patterns by their median duration and includes an example query for each pattern:
Use executions to distinguish recurring workload from isolated queries. A pattern with a high median duration, frequent executions, or high resource usage is a stronger candidate for investigation than a single slow run.As a quick inventory, the following query lists the slowest completed run for up to five distinct query patterns on the NYC Taxi dataset. It excludes dataset-loading statements and repeated runs of the same pattern. In the next step, you will narrow the query history to runs with the normalized_query_hash you selected above.
The query_duration_ms field contains the query duration in milliseconds. In these results, the longest-running query took 2,967 ms.You can also identify candidate queries based on resource usage rather than query duration:
This query ranks recent queries by memory usage and includes their CPU usage. Results vary by workload and deployment:
2

Choose a representative query run

A single slow run might be an outlier caused by an ad hoc query or temporary system load. Before inspecting the query plan, review several completed runs with the same normalized_query_hash, which is identical for queries that differ only by literal values. Choose a run that represents the pattern’s typical duration and resource usage.Replace the value assigned to selected_hash with the normalized_query_hash of the pattern you want to investigate:
  1. Find runs with similar read_rows and read_bytes.
  2. Compare query_duration_ms and memory_usage for those runs.
  3. Select the query_id whose query_duration_ms is closest to the median.
Historical query-log results can vary with cache state and system load, so use them to choose a query to investigate, not to compare optimization changes. If the query log does not contain enough completed runs, run the query a few times under similar conditions. The next guide, Isolate query bottlenecks, explains how to collect controlled measurements for comparing changes.
The example query-log results show that each candidate read approximately 329.04 million rows. For context, confirm the number of rows in the example table:
The table contains 329.04 million rows, approximately the same number reported in read_rows for each candidate. This suggests that the queries scanned most or all of the table, but it does not identify why those rows were read or whether that amount is appropriate for the query. Inspect the query plan next to see how ClickHouse selected and processed the data.
3

Inspect the execution plan

After choosing a representative run, use EXPLAIN to inspect how ClickHouse plans the query without running it. The output shows the operations ClickHouse expects to perform and how data moves between them, providing more context for the measurements in the query log.For a detailed introduction to the available output formats, see Understanding query execution with the analyzer. In this example, EXPLAIN shows how ClickHouse plans to read and filter the data and whether it can skip any of it.The output is a tree of operations that shows how ClickHouse expects to read, filter, and process the data. Child operations appear below their parents. Start with the deepest read operation, then follow the plan upward to see how ClickHouse transforms the data into the final result.For this example, inspect the calculated-speed query from the query-log results:
The output includes the following operations. Details such as the number of parts and granules depend on how the data is stored:
From the bottom up, the plan maps to the query as follows:
  1. ReadFromMergeTree reads from nyc_taxi.trips_small_inferred. The missing Indexes section, combined with read_rows matching the table’s row count, shows that ClickHouse reads the entire table.
  2. Filter shows the expanded expression for speed_mph > 30. For every row read, ClickHouse calculates the trip duration and speed, then keeps only rows above 30 miles per hour.
  3. Aggregating calculates the quantiles from the filtered trip_distance values.
This plan identifies three sources of work to test: reading every row, calculating speed_mph while filtering, and calculating the quantiles.

Next steps

Next, use Isolate query bottlenecks to learn how to test suspected sources of work under controlled conditions. It compares progressively simpler query shapes to identify which operations warrant further investigation.
Last modified on August 27, 2026