Crosshire
Monitoring
Run12 Monitoring · 3 of 3

Load and cost

Two weekly queries. One asks which statements are consuming the warehouse; the other asks which jobs became more expensive. Read together they usually answer each other.

Stand 29.07.2026

#Query history: expensive is not the same as slow

What
system.query.history records statements run on SQL warehouses and on serverless compute. Which compute types it covers has widened more than once, so it is not a fact to quote from memory — confirm it on the client's account against the reference below (Stand 29.07.2026). The useful weekly question is not what is the slowest query — it is what consumed the most warehouse time in total, which is a different query and usually a different culprit.
Why
Teams reliably optimise the wrong statement. The 20-minute quarterly report everyone complains about costs a fraction of the 3-second query a dashboard fires forty thousand times a week, and only one of the two appears at the top of this list. Ranking by total consumption moves the work to where the money is.
It also tells you who your consumers actually are. client_application routinely reveals a BI tool nobody mentioned in the requirements workshop, connected by someone who needed a number in a hurry.
How
sql
Where the warehouse hours went last week
SELECT
  LEFT(regexp_replace(statement_text, '\\s+', ' '), 120) AS shape,
  client_application,
  COUNT(*)                                              AS runs,
  ROUND(SUM(total_duration_ms)     / 3.6e6, 2)          AS wall_hours,
  ROUND(SUM(execution_duration_ms) / 3.6e6, 2)          AS exec_hours,
  ROUND(percentile(total_duration_ms, 0.95) / 1000, 1)  AS p95_seconds,
  ROUND(SUM(read_bytes) / POW(1024, 4), 3)              AS read_tib,
  SUM(IF(from_result_cache, 1, 0))                      AS cache_hits,
  SUM(IF(spilled_local_bytes > 0, 1, 0))                AS spilling_runs
FROM system.query.history
WHERE start_time >= current_timestamp() - INTERVAL 7 DAYS
  AND statement_type = 'SELECT'
  AND execution_status = 'FINISHED'
GROUP BY ALL
ORDER BY wall_hours DESC
LIMIT 20;
Read wall_hours against exec_hours before you read anything else. A large gap is time spent waiting for compute, not time spent computing — a warehouse sizing or auto-stop question, and no amount of SQL rewriting will touch it.
read_tib against the rows the statement returns is the file-pruning signal: a query over gold.fct_order_line filtered to one customer that reads terabytes is telling you the clustering keys do not match how the table is queried.
Doing it
  • Run this weekly and take the top three by wall_hoursCheck the wall-versus-exec gap before opening a query plan.
  • Cross-check the top consumers against gold.mv_salesRepeated hand-written aggregation is a measure that should exist in the metric view.
  • Check spilling_runs before resizing anythingSpill is a memory signal, and fixing the query is cheaper than buying a bigger warehouse for it.
  • Restrict SELECT on system.query.history deliberatelystatement_text can contain literal values, including personal data.
Embedding it
  • Let the team run the query in the review and pick the target themselvesThe choice between slowest and most expensive is the lesson, and it only lands if they make it.
  • Ask the owner of the top consumer to explain what the query is forRoughly a third of the time the honest answer retires the query.
  • Have them measure before and after any optimisation from this table rather than a stopwatchImprovement is then a number and not a feeling.
Defaults
  • Rank by total consumption, not by p95
  • Wall versus execution time first, query plan second
  • Repeated hand-written aggregation is a metric-view gap, not a tuning task
  • Treat query history as sensitive data and grant it like sensitive data
Gotchas
  • Optimising the slowest statementIt is almost never the expensive one, the work takes a week, the bill does not move, and the exercise is discredited internally before it reaches the query that mattered.
  • Reading total_duration_ms as query performanceIt includes waiting for compute, so a cold warehouse with aggressive auto-stop presents as slow SQL and someone rewrites a perfectly good transform before checking the split.
  • Result-cache hits flattening the pictureRows with from_result_cache = true take almost no time, so a dashboard refreshed twice looks half as expensive as it is. Keep the cache-hit count in the output.
  • Assuming the table has everythingStatements run inside a job on classic compute are not SQL warehouse statements, so 'we have no expensive queries' can mean the expensive work happens where this table does not look. Confirm coverage on the client's account first.
  • Grouping by raw statement_textEvery literal makes a new group, so the dashboard query that runs forty thousand times splits into forty thousand rows of one and never reaches the top of the list. The normalised 120-character prefix above is the cheap fix and it is only a heuristic — it splits the moment a literal lands inside the first 120 characters, and you notice because a query you know is heavy is missing from a list it obviously belongs on.

#Cost per domain, watched weekly

What
The tag taxonomy and the attribution query belong to the storage & cost route. Monitoring asks a narrower question on a schedule: which workload got more expensive this week, and by how much. Not which job — because usage_metadata.job_id does not identify the workloads this warehouse actually runs.
usage_metadata.job_id is populated for serverless jobs and for job compute. It is not populated for a task running on all-purpose compute, and Lakeflow pipeline usage carries dlt_pipeline_id / dlt_update_id instead. Two consequences, and this route walks into both:
The nightly chain reports as the cost of its gold step
dwh_daily above has two pipeline_task steps, and their DBUs bill under billing_origin_product = 'DLT' with no job_id — so a query grouped on job_id cannot see bronze or silver at all.
It is blind to exactly the population the audit route flags
The audit health check makes job tasks pinned to all-purpose compute a headline finding, and usage for those tasks has job_id = NULL. Grouped on job_id alone, this query cannot see them — which is how a cost review reports the warehouse is fine three weeks before an audit reports it is not.
Why
A monthly cost figure tells you spend rose. A weekly per-job delta tells you which change caused it, while the person who made that change still remembers making it. Two weeks later the same finding is archaeology, and the cluster configuration behind it has been copied into three more jobs.
It also keeps cost inside engineering. Once cost arrives only as a finance escalation, the conversation is about budget rather than about the join that stopped pruning — and engineers cannot fix a budget.
How
sql
Week-on-week cost movement per workload — jobs, pipelines, and the rest
WITH priced AS (
  SELECT
    -- job compute + serverless jobs carry job_id; Lakeflow pipelines carry
    -- dlt_pipeline_id; a task on ALL-PURPOSE compute carries neither.
    COALESCE(u.usage_metadata.job_id,
             u.usage_metadata.dlt_pipeline_id,
             '(unattributed)')  AS workload_id,
    CASE
      WHEN u.usage_metadata.job_id          IS NOT NULL THEN 'job'
      WHEN u.usage_metadata.dlt_pipeline_id IS NOT NULL THEN 'pipeline'
      ELSE 'unattributed'
    END                        AS workload_kind,
    u.billing_origin_product,
    u.custom_tags['domain']    AS domain,
    u.usage_date,
    u.usage_quantity * p.pricing.effective_list.default AS list_cost
  FROM system.billing.usage u
  JOIN system.billing.list_prices p
    ON  u.cloud    = p.cloud
    AND u.sku_name = p.sku_name
    AND u.usage_start_time >= p.price_start_time
    AND (p.price_end_time IS NULL OR u.usage_start_time < p.price_end_time)
  WHERE u.usage_date >= current_date() - INTERVAL 36 DAYS
    AND u.usage_date <  current_date() - INTERVAL 1 DAYS   -- closed days only
    -- deliberately NO "job_id IS NOT NULL": that filter is what hid the two
    -- pipeline tasks of dwh_daily and every all-purpose-compute job on the estate
),
weekly AS (
  SELECT
    domain,
    workload_kind,
    workload_id,
    billing_origin_product,
    SUM(IF(usage_date >= current_date() - INTERVAL 8 DAYS, list_cost, 0))     AS last_week,
    SUM(IF(usage_date <  current_date() - INTERVAL 8 DAYS, list_cost, 0)) / 4 AS prior_avg
  FROM priced
  GROUP BY ALL
)
SELECT
  domain,
  workload_kind,
  workload_id,
  billing_origin_product,
  ROUND(last_week, 2)                                    AS last_week,
  ROUND(prior_avg, 2)                                    AS prior_weekly_avg,
  ROUND(100 * (last_week / NULLIF(prior_avg, 0) - 1), 1) AS pct_change
FROM weekly
WHERE prior_avg > 0 AND last_week > 1.25 * prior_avg
ORDER BY pct_change DESC;
The (unattributed) bucket is not a rounding error to be hidden — it is the audit finding, priced. Resolve it through usage_metadata.cluster_id, because all-purpose compute bills against a cluster rather than against whichever job happened to be using it.
sql
The unattributed bucket, resolved to a cluster and an owner
WITH cluster_name AS (            -- system.compute.clusters is SCD2 as well:
  SELECT workspace_id, cluster_id, cluster_name, owned_by   -- one row per EDIT
  FROM system.compute.clusters
  QUALIFY ROW_NUMBER() OVER (
    PARTITION BY workspace_id, cluster_id ORDER BY change_time DESC) = 1
)
SELECT
  c.cluster_name,
  c.owned_by,
  ROUND(SUM(u.usage_quantity * p.pricing.effective_list.default), 2) AS list_cost
FROM system.billing.usage u
JOIN system.billing.list_prices p
  ON  u.cloud    = p.cloud
  AND u.sku_name = p.sku_name
  AND u.usage_start_time >= p.price_start_time
  AND (p.price_end_time IS NULL OR u.usage_start_time < p.price_end_time)
JOIN cluster_name c
  ON  c.workspace_id = u.workspace_id
  AND c.cluster_id   = u.usage_metadata.cluster_id
WHERE u.usage_date >= current_date() - INTERVAL 8 DAYS
  AND u.usage_date <  current_date() - INTERVAL 1 DAYS
  AND u.usage_metadata.job_id          IS NULL
  AND u.usage_metadata.dlt_pipeline_id IS NULL
  AND u.usage_metadata.cluster_id      IS NOT NULL
GROUP BY ALL
ORDER BY list_cost DESC
LIMIT 20;
Predictive optimization runs OPTIMIZE, VACUUM and ANALYZE for you on Unity Catalog managed tables, and it bills under its own billing_origin_product with no job_id and no domain tag. It is on by default for accounts created on or after 11.11.2024, and the rollout to older accounts is still completing — see the predictive-optimization entry in the currency register. On an older account it can therefore switch itself on mid-quarter, which presents as unexplained spend appearing in the untagged bucket. The response is not to hunt for the runaway job: it is to find the scheduled OPTIMIZE that is now doing the same work twice, and delete it.
Doing it
  • Schedule the query weekly into the same review as duration drift and query historyOne meeting, three numbers.
  • Read the unattributed bucket first, every weekIf it is large, the estate is running warehouse work on all-purpose compute and no per-job number on the page means anything yet.
  • Compare closed days only, as the WHERE clause above does
  • When pct_change fires, check the duration trend for the same job before opening the cost dataThe answer is usually already there.
  • Keep a one-line note per accepted increaseA rise everyone agreed to is not an incident, but only if someone wrote it down.
Embedding it
  • Hand over the query, not a dashboardA team that can edit the SQL owns the number; a team with a picture owns a picture.
  • Let the team explain the first fired row to the platform owner themselves, with you in the room and silent
  • Ask what they would delete if the budget were cut by a fifthThe answer is usually known, unwritten, and worth writing down.
Defaults
  • Weekly cadence, closed days only, list cost clearly labelled as list cost
  • COALESCE(job_id, dlt_pipeline_id) as the keyThe unattributed bucket is explicit, and read rather than filtered away.
  • Cost and duration reviewed side by side, in one session
  • A threshold of 25% week on week, tuned once after four weeks of observation
  • Every accepted increase carries a one-line reason
Gotchas
  • Including todayBilling data arrives with a lag of hours, so any window ending today is systematically low, and the first thing someone reports is a saving that does not exist.
  • Filtering on usage_metadata.job_id IS NOT NULLIt is populated for serverless jobs and job compute only — never for a task on all-purpose compute, and never for Lakeflow pipeline usage, which carries dlt_pipeline_id instead. That one WHERE clause drops the bronze and silver halves of dwh_daily and every job the audit route flags for running on all-purpose compute, and what is left still adds up to a plausible-looking weekly bill.
  • Reading a job-level view as the whole billusage_metadata.job_id is also NULL for SQL warehouse usage, so a dashboard costing more than the entire nightly chain is simply absent from this result. Run the warehouse split separately.
  • Presenting the unattributed bucket as a tagging problemIt is not: it is the cost of the compute nobody attached to a job, and answering it with a tagging workstream buys six weeks of tag review that cannot move a number that has no job to be tagged against. Resolve it through cluster_id first, then decide.
  • Attributing predictive optimization spend to a runaway jobIt carries no job_id and no domain tag, so it lands in the untagged bucket and reads as an attribution failure rather than a feature that switched itself on.
  • Reporting cost per job by job ownerThe moment a cost number appears next to a person's name in a management deck, the tags start being wrong, and the attribution you spent a month building is gone.
Platform facts on this page verified 29.07.2026 against the official documentation. Volatile claims are anchored to the currency register. This is section 3 of 3 in 12 Monitoring.