Crosshire
Reference
Reference22 Reference · 1 of 3

System tables

Databricks writes operational metadata into read-only Delta tables under the system catalog. They are the evidence base for cost, access, lineage and performance questions — no agent to install, no export to build.

Stand 29.07.2026
The system tables a warehouse team actually uses — with the two columns that decide whether you can build on one
TableOne row isAnswersStatusFree retention
system.billing.usagea metered usage recordwhere the money goes, per workspace, SKU and — if you tagged things — per domainGA365 days
system.billing.list_pricesa price for a SKU on one cloud over a validity intervalconverting DBUs into currency without hardcoding a rateGA365 days
system.access.auditan audited actionwho read, changed or granted what, and whenPublic Preview365 days
system.access.table_lineagea read/write between a table and a job or querywhat feeds this table, and what breaks if it changesGA365 days
system.access.column_lineagea column-level read/writewhich source column a PII column derives fromGA365 days
system.query.historya completed querywhich queries are expensive, and which are merely frequentPublic Preview365 days
system.compute.clustersa version of a cluster definition — a new row each time it changeswhat compute exists and how it is configuredGA365 days
system.compute.warehousesa snapshot of a SQL warehouse's properties, written whenever they changesize, scaling limits and auto_stop_minutes as they were on the day in question — the compute sweep's before/after baselineGA365 days
system.compute.warehouse_eventsa SQL warehouse state changewhether auto-stop is doing its jobGA365 days
system.compute.node_timelineone minute of utilisation for one nodewhether a cluster is idle, starved or simply the wrong sizeGA90 days
system.lakeflow.jobsa version of a job definition (SCD2 — a new row is emitted whenever a row changes)the inventory of what is scheduled, once you take the newest row per job_idGA365 days
system.lakeflow.job_run_timelinea period within a job run — several rows per run, because a run lasting more than an hour is recorded over multiple rowssuccess, duration and the run-duration trend, once you aggregate to run_idGA365 days
system.lakeflow.pipelinesa version of a pipeline definition (SCD2 — the same trap as jobs)which pipelines exist, and what one looked like before the editPublic Preview365 days
system.lakeflow.pipeline_update_timelinea period within a pipeline update — update_id, result_state (COMPLETED, FAILED, CANCELED), full_refresh_selectiondid the update finish, and was it a full refresh — as an ordinary table anyone with SELECT can read, rather than through event_log()Public Preview365 days
system.storage.predictive_optimization_operations_historyone OPTIMIZE, VACUUM or ANALYZE that predictive optimization ran for youwhat maintenance is already automatic — and what your scheduled job is now paying to duplicatePublic Preview180 days
system.data_quality_monitoring.table_resultsan anomaly-detection result for one tablewhich tables are stale, shrinking, or failing the checks nobody wrote by handPublic Previewindefinite
system.information_schema.*a catalog object as it is nowschema drift, grants, constraints, commentsGAcurrent state, no history

#Reading system tables without being misled by them

What
System tables are the cheapest evidence available on this platform: read-only, already populated, and queryable with plain SQL. The whole audit library next door is built on nothing else. The skill is not writing the query — it is knowing what a missing row means.
Why
The failure mode is subtle and expensive: reading absence as evidence. A system table returning no rows can mean the thing did not happen, or that the schema was enabled last month, or that lineage could not be inferred for that operation, or that the retention window rolled past it.
Report those as four different findings, because a client hears "there is no evidence anyone accessed the PII table" as "nobody accessed it". Those are not the same sentence, and only one of them is defensible.
How
sql
Cost per domain — the query that starts most engagements
SELECT
    u.custom_tags['domain']       AS domain,
    u.custom_tags['environment']  AS environment,
    ROUND(SUM(u.usage_quantity * p.pricing.effective_list.default), 2) AS cost_usd,
    count_if(p.sku_name IS NULL)  AS unpriced_rows   -- must be 0, or cost_usd is short
FROM system.billing.usage AS u
LEFT JOIN system.billing.list_prices AS p
       ON  u.cloud    = p.cloud          -- AWS | AZURE | GCP. Without this predicate a
                                         -- SKU priced on two clouds joins twice and the
                                         -- cost is DOUBLED, plausibly.
      AND u.sku_name = p.sku_name
      AND u.usage_end_time >= p.price_start_time
      AND (p.price_end_time IS NULL OR u.usage_end_time < p.price_end_time)
WHERE u.usage_date >= current_date() - INTERVAL 30 DAYS
GROUP BY ALL
ORDER BY cost_usd DESC;
Three details in that query are the difference between a number and a defensible number. pricing.effective_list.default — not pricing.default — is the key that resolves list and promotional price into the effective list price the cost is actually computed from. u.cloud = p.cloud stops a SKU that is priced on more than one cloud from fanning out and multiplying the total. And unpriced_rows exists because the join is a LEFT JOIN: an unmatched SKU contributes usage_quantity * NULL, SUM skips it, and the row costs nothing at all. Without that counter the query commits the exact failure the rest of this topic warns about — it reads absence as evidence, silently, in its own headline figure.
Run it on an untagged account and every row says NULL. That is not a broken query — it is the finding, and it is the argument for the tag taxonomy in the storage and cost route.
Two tables in the list above change what you can promise a rota. system.lakeflow.pipeline_update_timeline carries pipeline outcomes — update_id, result_state, full_refresh_selection — as an ordinary system table any rota member with SELECT can read, which is the way past the event_log() table-valued function being reachable only by someone who can already see the pipeline. system.compute.warehouses gives the warehouse configuration as it stood on a given day, so a compute recommendation can be evidenced against what was actually running rather than against what the console shows today. Both are Public Preview: use them, and write that down before a runbook depends on them.
Doing it
  • Establish the earliest row you can actually see before quoting any historical numberSELECT MIN(usage_date) FROM system.billing.usage, and the equivalent on every other schema you cite.
  • Join usage to list_prices on cloud, sku_name AND the validity intervalRead pricing.effective_list.default — prices change, SKUs are priced per cloud, and hardcoding a rate is wrong twice over.
  • Carry an unmatched-row counter beside every priced sumA missing price then shows up as a number instead of as a silently smaller total.
  • Check the retention of each table you build a recurring report onA quarterly comparison out of node_timeline is impossible at 90 days, and nobody discovers that until the second quarter.
  • State the window on every figure you report'EUR 4,200' means nothing; 'EUR 4,200 over the last 30 days' means something.
  • Treat an empty result as a finding to explain, never as a clean bill of health
Embedding it
  • Have the team run the cost query on their own account before you show them yoursThe NULL domain column makes the tagging argument better than any slide.
  • Ask 'what would make this query return zero rows?' about every check they writeIt is the single most useful habit for anyone reading operational data.
  • Have someone delete the u.cloud = p.cloud predicate on a multi-cloud accountWatch the total change without a single error. A cost figure that moves silently teaches join discipline in one sitting.
  • Hand over the queries as a repository they own, not as a documentThe ones they can re-run are the ones that survive.
Defaults
  • Enable the system schemas on day oneThe tables are free to hold, and waiting cannot make the history better.
  • Never hardcode a DBU priceJoin list_prices on cloud, SKU and the validity interval, and read effective_list.default.
  • Every priced sum ships with its unmatched-row count in the same result
  • Every reported figure carries its time window and the date it was run
  • Audit data is itself sensitive; grant it deliberately, as covered in the audit and lineage route
Gotchas
  • Quoting a historical figure from a schema enabled last weekWhether enabling backfills is undocumented (register key system-schema-retroactivity), so the period behind the number may be shorter than the client hears — and short in the flattering direction, because the missing history is missing spend. Report MIN(usage_date) beside the figure and the ambiguity disappears.
  • Telling a client their pre-enablement history is definitively goneThat is asserted all over the internet and confirmed by no first-party page; if it matters to the engagement, reproduce it on their metastore before you put it in a document.
  • Joining usage to list_prices on sku_name alonelist_prices carries a row per SKU per cloud (AWS, AZURE, GCP), so a SKU priced on more than one cloud fans out and the cost is multiplied by the number of matching clouds — a total that is too high, looks plausible, and nobody re-derives.
  • Reading pricing.default instead of pricing.effective_list.defaulteffective_list is the key that resolves list and promotional price into the price the cost is calculated from; the sibling keys are not what the invoice was computed against.
  • LEFT JOIN to list_prices with no unmatched-row counterAn unpriced SKU contributes usage_quantity * NULL, SUM skips it, and that usage costs zero for the rest of the engagement — absence read as evidence, inside the query that warns about it.
  • Counting rows in system.lakeflow.job_run_timeline as runsOne row is a period within a run, and a run longer than an hour is written over several rows, so run counts scale with duration and the slowest job also looks like the busiest. Aggregate to run_id first.
  • Joining system.lakeflow.jobs or system.lakeflow.pipelines without picking a versionBoth are SCD2 — a new row on every edit — so a plain join multiplies every run by the number of times the definition was ever changed, and the team that tunes its jobs most appears to run them most.
  • Reading an empty lineage result as 'nothing depends on this table'Both lineage tables are a subset: rows are emitted only when lineage can be inferred, so absence is not evidence of absence. Dropping a table on that basis is how a report dies silently.
  • Column lineage misses inserts that write explicit literal valuesA column populated by a constant appears to have no upstream at all — exactly the case you most want to spot in a PII trace.
  • Assuming system tables are consistent across cloudsAvailability and column sets differ between AWS, Azure and GCP; check on the client's cloud before promising a query works.
Platform facts on this page verified 29.07.2026 against the official documentation. Volatile claims are anchored to the currency register. This is section 1 of 3 in 22 Reference.