Crosshire
← Databricks Audit
— Crosshire / Learn · Tech · Databricks · Audit

The first audit — a guided runbook

New to an account and staring at 96 queries? Run these ten, in this order, and you will have a real findings sheet in about an hour and a half — no system-tables background required.

10 steps~90 min end to endzero prior setup

Why this order

Audit in three moves. Trust the numbers before you use them, chase the money while the account is fresh in your head, then check the risk that does not show up on a bill. Each step below tells you what a healthy account looks like, the one column to read, a synthetic example of a bad result, and the one decision to write down.

Before you start · the trust gate

Can You Trust the Bill?

~5 min · cost_restatement_trust_metric

Healthy account
One row per cloud. retracted_abs_quantity and restatement_usage_quantity are a small fraction of net_usage_quantity, and max_ingestion_date is yesterday or today.
The one column

restatement_usage_quantity ÷ net_usage_quantity

This is the restated %. Databricks corrects prior billing by appending RETRACTION and RESTATEMENT rows, not by editing history — so if a large slice of the period was later restated, or the feed is stale, every dollar figure you compute below is still moving.
A bad result
cloudnet_usage_quantityrestatement_usage_quantitymax_ingestion_date
AWS999,500.00180,000.002026-06-18
AZURE500,000.000.002026-07-05
Synthetic bad result — illustrative, not real account data
The AWS row was ~18% restated and its most recent ingestion is weeks old — the period has not settled. Any cost headline you quote from it is provisional.
Record this
If restated % is small and max_ingestion_date is current, proceed and quote net_usage_quantity to finance. If it is large or stale, mark the whole audit's dollar figures provisional and re-run when the feed catches up.
  1. 1
    Cost & Billingmoney~8 min

    Discount Realization by SKU (actual vs list)

    Per-SKU net DBUs alongside their default-rate dollars and list-rate dollars, so an admin can quantify how much the account's discount actually saves on each SKU — both rate bases come from list_prices (no account_prices/negotiated-rate table exists in this environment).

    Healthy account
    For every DBU-billed SKU, net_default_cost is a little below net_list_cost by a consistent margin, and no SKU shows large DBUs against a NULL cost.
    The one column

    net_list_cost − net_default_cost

    The gap is the account's directional discount realization per SKU. A SKU with real DBUs but NULL costs is not $0 — it has no matching active price row (a coverage gap).
    A bad result
    cloudsku_namenet_usage_quantitynet_default_costnet_list_cost
    awsENTERPRISE_ALL_PURPOSE_COMPUTE12,5005,500.006,875.00
    azurePREMIUM_JOBS_SERVERLESS_COMPUTE800NULLNULL
    Synthetic bad result — illustrative, not real account data
    The serverless SKU burns 800 DBUs but both cost columns are NULL — a priced-coverage gap, not free compute. Its spend is invisible in every downstream dollar rollup.
    Record this
    Record the realized-saving direction, and log every NULL-cost / large-DBU SKU as a coverage gap to chase — never as $0. This environment has no negotiated-rate table, so read the saving as directional.
  2. 2
    Cost & Billingmoney~8 min

    Per-Job DBU Cost Ranking

    Net DBU consumption and distinct run count per job per day, split by workspace, cloud, product line, and classic-vs-serverless placement.

    Healthy account
    DBUs are spread across many jobs with no single job_id dominating the day; the classic-vs-serverless split matches what you expect to be scheduled.
    The one column

    net_usage_quantity

    Jobs compute is usually the largest single product line on the bill. Ranking jobs by net DBUs shows exactly which job_id to target instead of the whole account.
    A bad result
    usage_dateworkspace_idjob_idis_serverlessnet_usage_quantitydistinct_runs
    2026-07-049876543210987654771044false1,180.001
    2026-07-041234567890123456849302false412.7524
    Synthetic bad result — illustrative, not real account data
    One classic job burns 1,180 DBUs in a single run (a heavy run to right-size); another burns 413 DBUs across 24 runs a day (over-scheduling / failed-run churn).
    Record this
    Record the top DBU jobs, resolve job_id to name + owner via system.lakeflow.jobs, and flag heavy-run and high-run-count offenders for right-sizing.
  3. 3
    Cost & Billingmoney~9 min

    Chargeback Coverage by Tag

    Net DBU consumption and record count broken down by every custom_tags key/value pair per usage_date, cloud, workspace, and product line — with untagged usage retained as NULL-key rows so the "% untagged" denominator is real.

    Healthy account
    Several tag_keys, each resolving to multiple distinct tag_values (real teams / cost centers), and the NULL-key (untagged) slice is small.
    The one column

    distinct tag_value per tag_key + the NULL-key slice

    Chargeback only works if spend attributes to a team. A key covering ~100% of DBUs with one value is an umbrella tag (zero real chargeback); NULL-key rows are the untagged spend nobody can be billed for.
    A bad result
    billing_origin_producttag_keytag_valuenet_usage_quantityrecord_count
    JOBScost_centershared-platform9,850.00412
    DLTNULLNULL1,560.2088
    Synthetic bad result — illustrative, not real account data
    cost_center covers almost everything but resolves to a single value — the “100% tagged” mirage. Meanwhile a whole DLT slice carries no tag at all.
    Record this
    Record the % untagged DBUs (NULL-key vs account total from cost_totals_by_sku_day) and any umbrella tag; require a per-team / cost-center dimension before chargeback means anything.
  4. 4
    Cost & Billingmoney~8 min

    Dollarizing DBUs at List Price

    Net DBU (and other-unit) consumption multiplied by the point-in-time list rate to produce a pre-discount list-dollar figure (net_list_cost) per usage_date x cloud x SKU x product x usage_type x unit x currency.

    Healthy account
    Every SKU/day row carries a populated net_list_cost in a consistent currency_code; the daily total tracks your expected run-rate.
    The one column

    net_list_cost

    This is the query that turns DBUs into money — it feeds every headline cost chart. A NULL here means a row the price join could not value, so the headline silently understates spend.
    A bad result
    usage_datecloudsku_nameusage_unitnet_usage_quantitynet_list_cost
    2026-07-01AWSENTERPRISE_SQL_COMPUTEDBU4,200.002,940.00
    2026-07-01AZUREENTERPRISE_JOBS_SERVERLESS_COMPUTEDBU800.00NULL
    Synthetic bad result — illustrative, not real account data
    The serverless row has 800 DBUs but a NULL list cost — an unpriced row (no price window matched). Sum the column blindly and your dollar headline is short by that slice.
    Record this
    Report the dollar figure as est · at list (pre-discount, DBU-only), not billed spend, and resolve every unpriced row before trusting the total.
  5. 5
    Cost & Billingmoney~8 min

    Serverless, Photon & Tier Premiums

    Net DBU consumption per usage_date x cloud x SKU x product line, split out by the serverless, Photon, jobs/SQL/DLT tier, and serverless performance-target choices so each premium lever can be sized.

    Healthy account
    Premium DBUs (is_serverless/is_photon true, fast performance targets) are a deliberate minority, and each premium row has a cheaper baseline sibling in the same product family.
    The one column

    net_usage_quantity on is_serverless / is_photon = true rows

    Serverless, Photon and fast tiers each carry a per-DBU premium. The question is whether that premium is buying real value or is just an unexamined default.
    A bad result
    cloudsku_nameis_serverlessis_photonnet_usage_quantity
    awsENTERPRISE_SQL_COMPUTE_SERVERLESS_(PHOTON)truetrue1,842.75
    awsENTERPRISE_SQL_PRO_COMPUTEfalsefalse612.40
    Synthetic bad result — illustrative, not real account data
    An always-Photon serverless SQL warehouse dwarfs its non-premium equivalent — a large premium running by default rather than by decision.
    Record this
    Record the premium DBU deltas per SKU family and decide, one family at a time, keep-the-premium vs downgrade. Multiply the delta by the SKU list rate to size it.
  6. 6
    Query Performancemoney~9 min

    Your Heaviest Statements, Ranked

    The top 1000 finished, non-cached SQL statements in the trailing window, ranked by execution time (a DBU-cost proxy), each carrying pruning, spill, shuffle, and scan counters plus a de-valued query text to diagnose the fix.

    Healthy account
    The heaviest statements show good pruning (pruned_filesread_files), zero spilled_local_bytes, and modest shuffle_read_bytes.
    The one column

    execution_duration_ms

    Databricks exposes no per-query dollar column, so execution time is the closest DBU-spend proxy — the top of this list is where money actually burns, and the attached counters name the fix.
    A bad result
    statement_idstatement_typeexecution_duration_msread_filespruned_filesspilled_local_bytesshuffle_read_bytes
    01ef…4a5bSELECT742,00018,40021005,368,709,120
    01ef…7081MERGE518,0009,600954,294,967,2961,073,741,824
    Synthetic bad result — illustrative, not real account data
    The top SELECT ran 12 minutes with almost no pruning (18,400 files read, 210 skipped) and 5 GB of shuffle — a full scan plus a bad join. The MERGE is spilling 4 GB to local disk (undersized warehouse).
    Record this
    Record the top statement_ids with their signal — poor pruning → partition/cluster; spill → size up or rewrite; big shuffle → fix the join. This is your tuning worklist.
  7. 7
    Computemoney~7 min

    Warehouse Idle-Tail Seconds

    Per SQL warehouse, the total seconds spent RUNNING (the auto-stop idle tail), STARTING (cold-start tax) and STOPPED, the worst single RUNNING gap, and event counts by state — all derived from state-transition events over the trailing :period_days window.

    Healthy account
    stopped_seconds dominates (the warehouse is off when idle); running_seconds is low and max_running_gap_seconds sits near the configured auto_stop_minutes × 60.
    The one column

    max_running_gap_seconds

    A warehouse bills compute for the whole stretch it sits RUNNING-but-idle before auto-stop suspends it. The worst single gap proves the auto-stop is set too high.
    A bad result
    warehouse_idrunning_secondsstopped_secondsmax_running_gap_secondsnet_dbusest_usd_list
    a1b2…5c6d5,40078,0001,800980490.00
    7c6b…9e8f9005,400300260130.00
    Synthetic bad result — illustrative, not real account data
    The first warehouse held a single 1,800-second (30-minute) idle tail while still RUNNING — pure waste, repeated every time it goes quiet, at ~$490 est · at list for the window.
    Record this
    Record the worst idle warehouses with their est · at list, cross-check auto_stop_minutes in sql_warehouse_config_current, and lower it so they suspend sooner.
  8. 8
    Jobs & Pipelinesmoney~7 min

    Failing Jobs Ranked by Wasted DBUs

    One row per job that had at least one failed run in the trailing window, with its distinct/failed run counts, the latest failure's termination code, total job DBUs, and a wasted-DBU proxy (job DBUs scaled by failed-run share).

    Healthy account
    Few or no jobs appear; where they do, failed_runs is a small share of distinct_runs and wasted_dbus_proxy is low.
    The one column

    wasted_dbus_proxy (and its est_usd_list)

    A failing job still bills its compute — a job that dies after 55 minutes pays for all 55. This ranks reliability fixes by dollar impact, not by raw failure count.
    A bad result
    workspace_idjob_idfailed_runsdistinct_runslast_failed_termination_codewasted_dbus_proxyest_usd_list
    12345678901234567755111010STORAGE_ACCESS_ERROR1,500750.00
    123456789012345688442225100CLUSTER_ERROR200400.00
    Synthetic bad result — illustrative, not real account data
    Job 775511 fails every run (10/10) and burns 1,500 wasted DBUs (~$750 est · at list) — all recoverable once the storage-access error is fixed.
    Record this
    Record the top DBU-burning failing jobs with their last_failed_termination_code; the code drives the fix (quota → raise limits; CLUSTER/STORAGE errors → fix config).
  9. 9
    Jobs & Pipelinesmoney~7 min

    Job Tasks On All-Purpose Compute

    Per-job, per-cluster task-run counts over the trailing 30 days, tagged with the cluster's source so you can spot job tasks running on all-purpose (UI/API) compute instead of cheaper job clusters.

    Healthy account
    Scheduled job tasks run on JOB clusters or serverless; there are few or no rows where cluster_source is UI or API.
    The one column

    cluster_source = UI / API with high task_runs

    All-purpose (interactive) compute is billed at a materially higher DBU SKU than jobs compute for the same work — every run on a UI/API cluster quietly overpays.
    A bad result
    workspace_idjob_idcompute_idcluster_sourcetask_runsnet_dbusest_usd_list
    12345678901234568844220715-091234-ab12cd34UI61214,2007,100.00
    12345678901234567755330715-090011-jobclu99JOB1,44030,50015,250.00
    Synthetic bad result — illustrative, not real account data
    Job 884422 ran 612 task-runs on a UI-created all-purpose cluster — ~$7,100 est · at list on a SKU it should never touch. (The JOB-source row alongside it is correct behaviour.)
    Record this
    Record every UI/API job cluster ranked by task_runs; migrate the highest-frequency offenders to job clusters or serverless — a recurring saving. Triage NULL cluster_source separately.
  10. 10
    Governance, Access & Securityrisk~7 min

    Run-As Privilege Escalation Watch

    A 30-day rollup of audited actions where the initiating principal (run_by) differs from the identity the action executed as (run_as), aggregated per service, action, and masked identity pair with event counts and first/last timestamps.

    Healthy account
    Sparse output. Every run_byrun_as pair is an explainable job or service-principal execution; nothing runs as a human owner unexpectedly.
    The one column

    run_by ≠ run_as pairs (event_count, last_event_time)

    A principal executing under another identity's authority is the audit fingerprint of privilege abuse, lateral movement, or a compromised service principal. Because the signal is genuinely sparse, every row is worth a look.
    A bad result
    service_nameaction_namerun_byrun_asevent_countlast_event_time
    accountsAccessControlupdatePermissionsop****__REDACTED__12026-07-05 11:47:02
    jobsrunNowda****@****sv****@****422026-07-05 02:15:11
    Synthetic bad result — illustrative, not real account data
    A permissions change was executed as a redacted identity, recently — the kind of one-off escalation to explain before anything else. The high-count jobs pair is likely a normal service principal, but confirm it.
    Record this
    Record each unexplained pair; confirm the run_by is authorized to act as the run_as, starting with the highest event_count / most recent. Zero rows → verify the audit table is actually populated before concluding “clean”.

Your findings sheet

One row per step. Fill status, est · at list impact and owner as you go; the finding and a suggested next query are pre-filled. Copy it into a spreadsheet and you have the deliverable.

Findings — first Databricks audit
FindingStatusEst · at list impactOwnerNext query
Discount Realization by SKU (actual vs list)cost_dollarized_by_sku_day
Per-Job DBU Cost Rankinglakeflow_jobs_on_all_purpose
Chargeback Coverage by Tagcost_totals_by_sku_day
Dollarizing DBUs at List Pricecost_actual_vs_list_by_sku
Serverless, Photon & Tier Premiumscost_dollarized_by_sku_day
Your Heaviest Statements, Rankedquery_pruning_effectiveness
Warehouse Idle-Tail Secondssql_warehouse_config_current
Failing Jobs Ranked by Wasted DBUslakeflow_retries_repairs
Job Tasks On All-Purpose Computecost_by_job
Run-As Privilege Escalation Watchaccess_admin_role_change_events
Tip: the Copy button gives you tab-separated rows — paste straight into Google Sheets or Excel.

Something look off? The war stories show three of these findings played out end to end, and the reference & glossary explains every term and the est · at list label.