Analytics & BI Tools · Chapter 10 of 18
Power BI internals: the model, and what DAX is really doing
The engine, the two evaluation contexts, and the three mistakes that account for most slow or wrong reports.
Power BI is easy to start with and unusually deep underneath, and the gap between those two facts is where most organisations get stuck. The people who can actually fix a broken report understand two things: how the storage engine works, and what a measure is.
Two engines, and which one you want your query in
Every query runs through two components, and the distinction is the single most useful diagnostic frame in the whole product:
- The storage engine operates on the compressed columnar data. It is multi-threaded, extremely fast, and can only do simple things — scan, filter, group, and simple aggregations. Work done here is nearly free.
- The formula engine handles everything the storage engine cannot: complex logic, iteration, anything row-by-row. It is single-threaded and it works on uncompressed data that the storage engine had to hand it.
- So the whole performance question is: how much work happened in the storage engine, and how many rows did it have to materialise for the formula engine? A query that runs entirely in the storage engine is fast regardless of data volume. A query that pulls a million rows out to iterate is slow regardless of how good your hardware is.
This is why the standard diagnostic is to capture the generated storage-engine queries and look at the row counts. One query returning a handful of rows is healthy. One query returning millions, followed by a long formula-engine step, is your problem — and you have found it in a minute rather than by guessing.
What a measure actually is
A measure is not a formula that computes a value. It is a formula that computes a value within a filter context supplied by wherever it is displayed. Once that clicks, most of the language stops being mysterious.
- Filter context is the set of rows currently visible — contributed by the row and column headers of the visual, by slicers, by page filters, and by anything else filtering the report.
- Row context exists inside a calculated column or an iterator, and means ‘the current row’. It does not filter anything on its own, which is the detail that surprises people.
- Context transition is the bridge, and it is what the
CALCULATEfunction does beyond its obvious job: it converts the current row context into an equivalent filter context. This is why wrapping an expression inCALCULATEcan change its result dramatically, and why an implicit measure reference inside an iterator behaves differently from the same expression written inline.
The practical consequence worth stating plainly: a total row is not the sum of the rows above it. It is the same measure re-evaluated with a different filter context. For a sum those coincide; for a distinct count, an average, or any ratio, they do not — and explaining that to a business user who thinks the report is broken is a recurring part of the job.
The three mistakes that cause most problems
1. A calculated column where a measure belongs. A calculated column is computed once at refresh and stored in the model, so it adds cardinality and size permanently — and a high-cardinality calculated column on a large fact table can single-handedly double a model. A measure is computed at query time and stores nothing.
The rule: use a measure unless you genuinely need to slice, filter or group by the value. If it appears on an axis or in a slicer, it must be a column. If it only ever appears as a number, it should be a measure.
2. Row-by-row iteration over a large fact table. Iterator functions evaluate an expression per row. Over a dimension of a few hundred rows that is free; over a hundred-million-row fact table it forces the formula engine to do the work the storage engine should have done. The fix is almost always to restructure so the aggregation happens inside the scan — or to precompute the value upstream, which is frequently the right answer and the one people resist.
3. Bidirectional relationships set everywhere. They are enabled to make one visual work and then left on, and they create genuinely ambiguous filter paths — the engine cannot determine which route a filter should take. Most ‘the numbers are wrong’ reports in a mature estate are relationship ambiguity, not arithmetic. Prefer single-direction relationships plus an explicit CROSSFILTER in the one measure that needs it, so the exception is visible and local rather than global and silent.
Refresh, and the change that makes large models viable
A full refresh reloads everything. For a large fact table that is both slow and unnecessary, since historical data does not change. Incremental refresh partitions the table by date and reloads only recent partitions.
Two details that decide whether it works. It relies on the source honouring the date predicate efficiently — if the filter is not pushed down to the source, you have gained nothing and added complexity. And it requires the historical data genuinely to be immutable; late-arriving corrections to old periods will be silently missed, which is a data-quality bug that surfaces months later as an unexplained discrepancy.
The related capability worth knowing about is a mode that reads directly from open table formats in the lake without either importing or generating a warehouse query per interaction. It is genuinely useful and it has a specific caution: it can silently fall back to ordinary query mode when a query exceeds a guardrail, and the user simply experiences a slow report with no indication why. If you deploy it, monitor the fallback rate as a first-class metric rather than trusting the mode name.
Where Power BI genuinely hurts
- Version control and code review. Report files have historically been binary, so diffing and reviewing changes is awkward. Newer project formats improve this considerably and adoption is uneven — if you are setting up a new estate, choose the text-based format from day one, because retrofitting it across hundreds of reports is a project.
- Governance drift. The same accessibility that drives adoption produces hundreds of near-duplicate models, each with its own definition of revenue. This is not a tool defect; it is the predictable consequence of self-service without a shared semantic layer, and it is covered in the next chapter.
- Capacity behaviour. Capacity-based licensing smooths and bursts usage, then throttles when a sustained overdraft accumulates — so the symptom is that reports become slow for everyone some time after the expensive thing ran. Diagnosing that without understanding the smoothing model is close to impossible, and it is the most common confusing operational issue on the platform.
Check yourself