Someone says "the warehouse is slow". These queries show whether the time went on waiting, compiling, scanning, spilling or fetching results, and which user, job or dashboard caused it.
Records typically arrive within an hour. For a query running right now, use Query History in the workspace sidebar instead.
Note
Column names can change between releases. If a query fails on a column, check the current table reference or run
DESCRIBE system.query.history.
Read the numbers first
| Reading | What it means | Fix |
|---|---|---|
High waiting_at_capacity_duration_ms | Compute is up but full | Raise max clusters, or move heavy work to its own warehouse |
High waiting_for_compute_duration_ms | Waiting for compute to be provisioned | Check auto-stop and start-up behaviour; consider serverless |
High compilation_duration_ms | Slow metadata loading or optimization | Simplify nested views; compact tables with many files |
High execution_duration_ms, few files pruned | Scanning too much | Add selective filters; use liquid clustering on the filter columns |
High spilled_local_bytes | Ran out of memory and wrote to disk | Reduce the data joined or aggregated; try a larger warehouse |
High shuffle_read_bytes | Large joins or aggregations | Filter earlier; review the join order in the query profile |
High result_fetch_duration_ms | The client is pulling a big result | Aggregate or limit the result; check the BI tool's settings |
Slowest queries, with a time breakdown
SELECT statement_id,
workspace_id,
executed_by,
compute.warehouse_id,
client_application,
round(total_duration_ms / 1000, 1) AS total_s,
round(waiting_for_compute_duration_ms / 1000, 1) AS wait_compute_s,
round(waiting_at_capacity_duration_ms / 1000, 1) AS queued_s,
round(compilation_duration_ms / 1000, 1) AS compile_s,
round(execution_duration_ms / 1000, 1) AS execute_s,
round(result_fetch_duration_ms / 1000, 1) AS fetch_s
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 24 HOURS
AND execution_status = 'FINISHED'
ORDER BY total_duration_ms DESC
LIMIT 25;
To open the profile, log in to the workspace in workspace_id, because the table covers every workspace in your account in the same region. Then go to Query History, paste the statement_id into the Statement ID field, click the query and choose See query profile.
Queueing per warehouse, per hour
SELECT compute.warehouse_id,
date_trunc('HOUR', start_time) AS hour,
count(*) AS statements,
count_if(waiting_at_capacity_duration_ms > 0) AS queued_statements,
round(percentile_approx(waiting_at_capacity_duration_ms, 0.95) / 1000, 1) AS p95_queued_s
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 7 DAYS
AND compute.type = 'WAREHOUSE'
GROUP BY ALL
HAVING queued_statements > 0
ORDER BY p95_queued_s DESC
LIMIT 50;
If queueing appears at the same hour every day, look for a scheduled refresh or a batch job at that time before you add clusters.
Queries that scan too much
SELECT statement_id,
executed_by,
read_files,
pruned_files,
round(100 * try_divide(pruned_files, read_files + pruned_files), 1) AS pct_files_pruned,
round(read_files_bytes / 1e9, 2) AS gb_read,
read_rows,
produced_rows,
round(total_duration_ms / 1000, 1) AS total_s
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 24 HOURS
AND statement_type = 'SELECT'
AND from_result_cache = false
AND read_files > 0
ORDER BY read_files_bytes DESC
LIMIT 25;
A low pct_files_pruned with many read_rows and few produced_rows means files were read and then mostly filtered out. That usually points to a filter that doesn't match the table's clustering, or a function wrapped around the filter column.
Tip
To compare with the table size, use
pruned_files_bytes + read_files_bytes. Don't useread_bytesfor this: it includes disk-cache data and retries, so it can be larger than the table.
Memory pressure: spill and shuffle
SELECT statement_id,
executed_by,
compute.warehouse_id,
round(spilled_local_bytes / 1e9, 2) AS gb_spilled,
round(shuffle_read_bytes / 1e9, 2) AS gb_shuffled,
round(total_task_duration_ms / execution_duration_ms, 1) AS avg_parallelism,
round(execution_duration_ms / 1000, 1) AS execute_s
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 24 HOURS
AND spilled_local_bytes > 0
AND execution_duration_ms > 0
ORDER BY spilled_local_bytes DESC
LIMIT 25;
Who is using the warehouse
SELECT coalesce(client_application, 'unknown') AS client,
executed_by,
query_source.job_info.job_id AS job_id,
query_source.dashboard_id AS dashboard_id,
query_source.genie_space_id AS genie_space_id,
count(*) AS statements,
round(sum(total_task_duration_ms) / 3600000, 2) AS task_hours
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 7 DAYS
AND compute.warehouse_id = '<warehouse-id>'
GROUP BY ALL
ORDER BY task_hours DESC
LIMIT 25;
client_application comes from what the client reports, so treat it as a hint. If your teams set query tags, filter on them with query_tags['team'] = 'finance'. Tags are populated for SQL warehouses and serverless notebook and job compute.
Failures grouped by error
SELECT coalesce(nullif(regexp_extract(error_message, '\\[([A-Z0-9_.]+)\\]', 1), ''),
left(error_message, 80)) AS error,
count(*) AS failures,
count(DISTINCT executed_by) AS principals,
min(start_time) AS first_seen,
max(start_time) AS last_seen
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 24 HOURS
AND execution_status = 'FAILED'
GROUP BY ALL
ORDER BY failures DESC;
A new error with a recent first_seen and many principals usually means a shared change, such as a dropped table, a revoked grant or a network rule. execution_status has three values: FINISHED, FAILED and CANCELED.
Jobs slower than their weekly baseline
SELECT query_source.job_info.job_id AS job_id,
round(percentile_approx(execution_duration_ms, 0.5)
FILTER (WHERE start_time >= current_timestamp() - INTERVAL 1 DAY) / 1000, 1) AS p50_last_day_s,
round(percentile_approx(execution_duration_ms, 0.5)
FILTER (WHERE start_time < current_timestamp() - INTERVAL 1 DAY) / 1000, 1) AS p50_prior_s,
count(*) AS statements
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 8 DAYS
AND query_source.job_info.job_id IS NOT NULL
AND execution_status = 'FINISHED'
GROUP BY ALL
HAVING p50_last_day_s > 2 * p50_prior_s
ORDER BY p50_last_day_s DESC;
The 2x threshold is only a starting point. A job's statements can vary a lot, so check the job's run history before raising a problem.
Cache hit rates per warehouse
SELECT compute.warehouse_id,
count(*) AS statements,
round(100 * count_if(from_result_cache) / count(*), 1) AS pct_result_cache,
round(avg(read_io_cache_percent) FILTER (WHERE read_files > 0), 1) AS avg_disk_cache_pct
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 7 DAYS
AND compute.type = 'WAREHOUSE'
GROUP BY ALL
ORDER BY statements DESC;
What the table can and can't tell you
| Item | Detail |
|---|---|
| Default access | Admins only |
statement_text | Returns <REDACTED> unless you are an account admin or in the databricks_pii_access account-level group |
| Coverage | SQL warehouses, serverless notebooks and jobs, Lakeflow pipelines on serverless or classic compute. Don't assume classic notebooks and jobs are covered |
compute.cluster_id | Never populated, so you can't join to a cluster on it |
| Customer-managed keys | statement_text and error_message need extra setup to read. Older records can't be decrypted |
| Retention | Not stated on the table reference. Check how far back your data goes with the query below |
SELECT min(start_time) AS oldest_record FROM system.query.history;
Prevent it next time
Give team leads access through a view instead of the base table. Databricks recommends a dynamic view per user or group:
CREATE OR REPLACE VIEW ops.monitoring.query_history_scoped AS
SELECT statement_id, executed_by, compute, execution_status, start_time,
total_duration_ms, waiting_at_capacity_duration_ms, execution_duration_ms,
read_files, pruned_files, spilled_local_bytes, error_message
FROM system.query.history
WHERE is_account_group_member('platform-admins')
OR executed_by = current_user();
GRANT SELECT ON TABLE ops.monitoring.query_history_scoped TO `data-engineers`;
Caution
Query text and error messages can contain customer values used as literals. Keep
statement_textout of shared views unless the audience is allowed to see that data. If customer-managed keys are enabled on thesystemcatalog, Databricks warns that existing grants onsystem.queryandsystem.query.historyare removed, so grant them again afterwards.
Then make the checks routine:
- Save the queueing and failure queries as Databricks SQL alerts, so problems show up without anyone running them by hand.
- Ask teams to set query tags on scheduled workloads, so you can trace a load to an owner quickly.
- Review the top scans each month and fix the table layout for the worst offenders.