Before you begin
Start with a recurring slow-query pattern you want to investigate. If you have not identified one yet, Diagnose slow queries walks through the process. To run the examples in this guide as written, create and load thenyc_taxi.trips_small_inferred table if you have not already done so:
Set up the example dataset
Set up the example dataset
The source Parquet file is approximately 5.8 GB. Loading it can take several minutes, depending on your network and available resources.
ORDER BY (), so its date filter cannot use an ordering key to eliminate data during the read. Use the example to practice the comparison method rather than as a performance target.
How it works
Progressively simplifying a query lets you compare its duration before and after removing a stage of work. The differences help you decide whether to investigate scanning and filtering, grouping, aggregate calculations, or later work such as sorting and output formatting:- Run the original query to establish the baseline measurements.
- Keep
GROUP BY, replace the query’s aggregate calculations withcount, and remove later operations such as sorting and output formatting. - Remove the grouping and run an ungrouped
countto approximate the work retained by scanning, filtering, and any joins.
SELECT block at a time: preserve equivalent data sources and filters, remove one operation at a time, and verify the execution plan after each change.
These differences are diagnostic estimates, not exact measurements of ClickHouse execution stages. Changing the query can alter its execution plan, columns read, and data passed between stages. Use the results to form a hypothesis. Then validate it with query logs and
EXPLAIN.Establish a repeatable baseline
Use the following practices to make the measurements comparable:- Keep the
FROM,JOIN,PREWHERE, andWHEREclauses unchanged so every comparison uses the same data and time range. - Run each version of the query several times under similar system load.
- Keep cache conditions consistent. Either run each version of the query before recording measurements or disable the caches listed below. Do not compare cached and uncached runs.
- Record a representative duration, such as the median across repeated runs after any warm-up runs, rather than relying on the fastest or slowest result.
- Change one variable at a time so that you can associate a performance difference with a specific change.
count in run C does not use an optimized execution plan that bypasses the scan you intend to compare.
These
SET statements apply only to the current session. Run all comparison queries in that session, or apply the same settings to every run. The filesystem cache setting does not disable the operating system’s page cache or every ClickHouse cache. When you finish, close the dedicated session or restore each setting to its previous value.-
Assign a unique query ID to every run, or record the ID generated by your query interface. For example, identify repeated runs as
bottleneck-a-1,bottleneck-a-2, andbottleneck-a-3. Withclickhouse-client, pass--query_id your-query-idwhen you execute a query. - Execute each comparison query several times under the same conditions. Keep warm-up runs separate from the measured runs.
-
Flush the query log before looking up recently completed queries:
If you cannot run
SYSTEM FLUSH LOGS, wait for the query log to flush automatically, then retry the lookup. If the record never appears, verify that query logging is enabled, that you can readsystem.query_log, and that you are querying the node that ran the query. -
Look up the completed record for each query ID.
system.query_logrecords bothQueryStartandQueryFinishevents for a completed query. Filter forQueryFinish, which contains the final duration, rows and bytes read, and peak memory: -
For each version of the query, use the median duration from the measured runs. Record
read_rows,read_bytes, and peak memory from the run closest to that median so that the measurements remain tied to an actual run.
For distributed queries,
memory_usage in the initiating query’s QueryFinish record is not a cluster-wide peak. Use initial_query_id to inspect child QueryFinish records on participating nodes.system.query_log for more information about its fields and configuration.
- Table
- CSV
Run progressively simpler queries
To demonstrate all three comparisons, the example uses the grouped date-range workload. You can apply the method to a different query without following the worked example. If the query does not containGROUP BY, skip run B as described below.
1
Run A: Measure the original query
Run the complete query without changing its filters, grouping, aggregate expressions, sorting, or output. This establishes the baseline duration, rows and bytes read, and peak memory usage.This query groups trips by payment type and calculates several aggregate values:Record the query’s measurements as run A.
2
Run B: Retain grouping with count
Preserve the query’s Run B still scans and filters the data, performs any joins, and constructs the groups. Compare its duration with run A to estimate the contribution of the original aggregate expressions and work after aggregation. Also compare
FROM, JOIN, PREWHERE, WHERE, and grouping keys. Replace its aggregate expressions with a grouped count. Remove work after aggregation, including the original sorting and output expressions.read_bytes, because removing aggregate expressions can remove columns from the read.If the original query does not contain a GROUP BY, there is no grouping stage to isolate. Skip run B and compare the original query directly with run C.3
Run C: Remove grouping
Remove Run C provides a baseline for the operations its plan retains, not an isolated measurement of scanning or filtering. Compare it with run B to estimate the contribution of grouping. Also compare
GROUP BY and return a single count. Keep the FROM, JOIN, PREWHERE, and WHERE clauses unchanged so that the remaining work is comparable.read_bytes, because removing the grouping key can reduce the columns read. The returned count shows how many rows reach aggregation after the preserved filters and joins.Before interpreting run C, confirm that its execution plan reads the intended data source and applies the preserved filters. A projection or metadata-based count can change the work performed. For a scan-based baseline, disable the optimization shown in the plan for all three runs: use optimize_use_implicit_projections = 0 for an implicit projection, optimize_use_projections = 0 for an explicit projection, or optimize_trivial_count_query = 0 for an unfiltered count served from table metadata.If run C remains slow, investigate the operations retained in it, beginning with scanning and filtering. Use query logs and EXPLAIN to validate the suspected bottleneck before changing the query.Interpret the differences
Compare representative durations from repeated runs instead of subtracting two individual timings. Large, consistent differences indicate where to investigate next:Compare rows read with the count result
Compareread_rows for run C with the value returned by its count. For example, if read_rows is 100 million and count returns 1 million, ClickHouse scanned approximately 100 source rows for every row counted. This shows that the filter rejected most of the rows read from the table, but it does not identify why. This ratio is intended for straightforward single-table scans. For queries with multiple data sources or projections, interpret read_rows using the execution plan instead.
For ClickHouse 25.9 and later, disable the query-condition cache and dynamic application of data-skipping indexes before inspecting index usage:
EXPLAIN indexes = 1 to see which indexes ClickHouse used and how many parts and granules each index eliminated. If ClickHouse selected more granules than expected, inspect whether the filters align with the table’s ordering key and whether partition pruning or a data-skipping index could eliminate more granules. If the plan has no Indexes section, EXPLAIN did not report index pruning for that query. A full-table analytical query, by contrast, is expected to read most of the table.
Validate the suspected bottleneck
After the comparison points to a likely bottleneck, validate it before changing the schema or query. Use evidence appropriate to the suspected source of latency:- For a scan or filtering bottleneck, use
EXPLAIN indexes = 1with the settings described above to see which indexes ClickHouse uses and how many parts and granules each index eliminates. Check whether the plan uses an implicit projection instead of the expected scan. - For a grouping or aggregation bottleneck, inspect relevant query profile events and peak memory usage.
- If run C remains slow and contains joins, compare it with a diagnostic query that removes one join at a time. A large decrease in duration suggests that the removed join contributes significant work. Because removing a join changes the query’s meaning, use this comparison only to isolate timing and interpret changes in row count separately.
- For a bottleneck in another operation retained by run C, inspect the execution plan and relevant query profile events.
EXPLAIN. Apply one targeted change, then repeat runs A, B, and C under the same conditions. Confirm that the change reduced the intended work and did not move the bottleneck elsewhere.