The model is the product, the report is the skin
A semantic model (the object formerly called a 'dataset') is a tabular data model: tables, columns, relationships, and DAX logic. Reports are thin consumers pointed at it. When someone says 'the report is wrong or slow', the answer is almost always in the model, not the visual.
The VertiPaq engine (why import models are fast)
For import models, data is loaded into VertiPaq — an in-memory, columnar, compressed store. It compresses column-by-column (dictionary + run-length encoding), so:
- Cardinality is the enemy. High-cardinality columns (unique IDs, precise datetimes, free text) compress poorly and bloat the model. Splitting a datetime into date + time, or dropping unused columns, can shrink a model dramatically.
- Narrow beats wide. Fewer columns, lower cardinality, and the right data types are the whole performance game before you write a single measure.
Star schema: the modelling discipline
The correct shape is a star schema — narrow fact tables (transactions, events) surrounded by dimension tables (date, product, customer) joined by single-direction one-to-many relationships. This is not a stylistic preference; VertiPaq and the DAX engine are built for it. A flat, wide single table or a tangle of bidirectional relationships is the root cause of most slow, wrong models.
Measures vs calculated columns
- Measures are DAX evaluated at query time, in the filter context of the visual — cheap in memory, the right default for aggregations.
- Calculated columns are materialised into the model at refresh — they cost memory and cardinality. Overusing them (especially row-by-row logic that should be a measure) bloats and slows the model.
Failure modes
- The one-big-flat-table model. No dimensions, everything denormalised — huge, slow, and impossible to secure with row-level security cleanly.
- Bidirectional relationships everywhere. Turned on to 'make it work', they create ambiguous filter paths, wrong totals, and RLS holes.
- Calculated-column abuse. Logic that belongs in a measure materialised into columns, ballooning model size against capacity memory limits.
- Unmanaged size. Import models have per-licence/per-capacity memory ceilings; a model that grows past them fails refresh or won't load — a silent operational cliff.
- Auto date/time tables. The hidden per-column date tables Power BI generates can silently double model size.
What to watch (proofs)
- Model inventory and size:
Get-PowerBIDataset -Scope Organizationlists every semantic model; the admin scanner (admin/workspaces/getInfo) returns tables, columns, measures and relationships for lineage and bloat review. - Refresh health: a model's refresh history (success/failure/duration) via
Get-PowerBIDatasetrefresh endpoints — the first read for 'slow/wrong'. - Storage mode per table: import vs DirectQuery vs Direct Lake, exposed by the scanner — the performance/freshness trade-off made explicit.
- Orphaned and duplicate models: models with no dependent reports, or many near-identical copies — the sprawl that inflates capacity memory.
- Cross-workspace lineage: which reports point at a model in another workspace — 'who can see the data' follows the model's workspace, not the report's.