“We query your warehouse live” sounds better than “we keep a copy”, and for a decade the industry has sold the first. It is worth examining why almost every serious BI deployment ends up with an extract anyway, usually after trying it the other way.

What actually happens without a copy

Reporting load lands on the system that earns money, at the worst possible times — Monday morning, month end, the hour before the board meeting. Three consequences, all predictable:

ConsequenceHow it shows upThe usual workaround
Reporting competes with productionDashboard queries queue behind loads and transformations, or worse, in front of them.A read replica — which is a copy, with extra steps
Cost scales with readersPer-byte or per-slot warehouse billing turns each dashboard open into a charge, repeated for identical questions.Rationing who can open dashboards
Someone else’s outage is your outageThe warehouse is down for maintenance; every dashboard in the company is blank.Explaining it, repeatedly

Almost every organisation that runs BI live against its warehouse eventually builds a read replica or a reporting schema. That is an extract. The question was never whether to copy — only whether the copying is a product feature or a project.

What a governed copy is

Not a cache and not a backup. A deliberate, versioned, aggregated copy in your cloud storage, in an open format.

PropertyWhat it meansWhy it matters
Parquet, in your bucketColumnar files on S3, GCS or Azure Blob, in your own account.Remove the product and every file is still readable
Versioned by refreshEach refresh writes a new prefix; the previous one stays until a lifecycle rule expires it.A bad refresh is a rollback, not an incident
Aggregated to the grain you needThe extract is the shape the dashboards ask for, not a verbatim copy of a fact table.Megabytes, not terabytes
Read by an embedded engineColumnar reads touch only the columns and row groups a query needs.Milliseconds, without a cluster
Immutable once writtenA version never changes. A refresh creates a new one.A cached result keyed on the version can never be wrong
Datasource
A datasource page showing status ready, 600 rows in one table, the extract size and version string, when it expires, and that the runtime may be served to a browser or desktop
The version string is the S3 prefix. Everything about the copy is visible on this page — how many rows, how big, how old, and whether it may be served to a browser or desktop runtime.

Immutability is what makes caching safe

This is the design decision that pays for itself repeatedly. Because a version never changes, a result computed against it is valid for as long as that version exists. The cache key includes the version, so a refresh is the invalidation — there is no cache-busting logic to get wrong.

And it composes with security

The key covers the source version, the SQL and the bound values — and the person’s row and column rules are already inside that SQL. So two people with different access compute different keys. Nobody had to add a check; it falls out of keying on the compiled statement. Why the compilation order matters →

Note the fourth row. It got a live connection because the source happened to be Postgres. Had the same operational data lived in Snowflake or BigQuery, the honest answer would have been a fifteen-minute extract with the cadence stated on the page — not a live connection, because there is no attach path and pretending otherwise would have produced a slow dashboard and a surprised finance team.

flowchart LR
    R["A refresh runs"] --> V["New version
new storage prefix"] V --> I["The old version is
untouched, still readable"] V --> K["Cache keys include
the version"] K --> N["Every stored result for
the old version is now
unreachable, not wrong"] I --> RB["A bad refresh
is a rollback"] style V fill:#eef1ff,stroke:#c7cdf2 style N fill:#e6f7f2,stroke:#9fd8c9 style RB fill:#e6f7f2,stroke:#9fd8c9
Why immutability is load-bearing rather than tidy. A refresh is the cache invalidation, so there is no cache-busting logic to get wrong — and the previous version is still sitting there if the new one is bad.

The staleness budget

The cost of a copy is that it is as current as its last refresh. That is a real trade and the right way to handle it is to make it explicit rather than to pretend it away.

A staleness budget is a sentence: this dashboard may be up to four hours old. Agreed with the people reading it, set as a refresh schedule, and visible in the product. Most reporting has a budget of hours and nobody has ever written it down.

flowchart LR
    Q["What is this
dashboard for?"] --> A{"Would a number
four hours old
change the decision?"} A -->|"No — most reporting"| E["Extract
fast, cheap, no load on source"] A -->|"Yes — operational"| L{"Is the source
Postgres, MySQL
or Redshift?"} L -->|"Yes"| LV["Live connection
attached read-only"] L -->|"No"| F["Extract, refreshed often
and say so on the page"] style E fill:#e6f7f2,stroke:#9fd8c9 style LV fill:#eef1ff,stroke:#c7cdf2 style F fill:#fff4e6,stroke:#f0cb9a
The decision, honestly. Most branches end at the extract, and the one that does not depends on which database you are on rather than on how much you want it.

When to use a live connection

Genuinely operational screens — a queue depth, an order status board, something somebody acts on within minutes. For those, the engine attaches the source read-only and queries it directly, copying nothing and caching nothing, because caching something whose whole point is being current is a wrong answer that looks right.

The constraint is which databases can be attached: PostgreSQL, MySQL and Redshift. Snowflake, BigQuery, Synapse, Azure SQL and Databricks are extract-only. If your operational data is in one of those, the honest answer is a frequent refresh with the cadence stated on the page, not a live connection. What that means per cloud →

Saying so on screen

A stale number that looks current is worse than a blank panel. So a dashboard paints from the last known result while fresh ones are fetched — and says, above the panels, that what is on screen is not yet current, with the time it was computed.

Dashboards
A dashboard with three headline cards, a sales and profit trend, and breakdowns by region and category
The page fills immediately and then corrects itself. A reader never waits at a blank rectangle, and never mistakes last night’s figure for this morning’s, because the product tells them which they are looking at.

What the copy costs, measured

Millisecondsengine time for a panel over an aggregated extract
Low gigabytestypical extract size, aggregated
A few dollarsmonthly object storage for it
Zerowarehouse compute on a dashboard open

The last figure is the one to take to whoever owns the warehouse bill. After a migration to extracts, warehouse spend usually falls, because the thing that was generating unpredictable interactive load has stopped. Where the time actually goes →

What we have not solved

  • Incremental refresh by partition is unfinished. Today a refresh rewrites the extract. For a very large fact table that is the difference between comfortable and painful, and it is the most requested thing on this list.
  • An extract is a point in time, not a stream. If you need change data capture semantics, this is the wrong layer and you should land that upstream.
  • Someone has to own the cadence. The product will refresh on whatever schedule you set, including a bad one. “Nightly” is a decision, not a default, and the people reading the dashboard should know what it is.
  • Live Databricks is not built. It needs a passthrough we have not written.

The question to settle per datasource

Not “live or extract” as an architectural religion. Per datasource: what is the oldest this number can be before a decision made on it would be wrong? Write the answer down, set the schedule to match, and put it where the readers can see it. Almost every disagreement about freshness is really a disagreement that nobody has had out loud.

What runs against the copy →  ·  Where it lives on AWS →

← All posts   Request a demo