Two lines of DAX, 5 seconds to load
KPI card, 5 seconds to load. Performance Analyzer confirms ~5,200ms. The measure behind it:
Completed Transactions Revenue =
CALCULATE(
SUM('Transactions'[Revenue]),
'TransactionState'[State] = "Completed"
)
So what's happening?
The fact table has one row per product - TransactionID repeats. The state table has one row per state change - TransactionID repeats there too. Neither side is unique, so the relationship is many-to-many.
The storage engine can't do a direct lookup. It builds a cross-product intermediate table first, then filters it. With 500K transactions and 2M state records that intermediate result is enormous. No segment-level filtering either - full table scan on every evaluation. Each slicer on the page forces the engine to resolve the join from scratch.
Performance Analyzer: 4,800ms in the Storage Engine, 400ms in the Formula Engine. 92% of the time is spent scanning and joining, not executing DAX. 47 SE queries for two lines of code.
The fix is a bridge table with one row per TransactionID - clean unique key on both sides, direct lookup, no cross-product. The measure doesn't change at all.
Other model decisions that degrade everything
- High cardinality columns in fact tables - email addresses, URLs, natural keys destroy compression. Surrogate integer keys compress significantly better and are faster on every scan
- Wide fact tables with unreferenced columns - columns that serve no report purpose still consume memory and participate in scans
- Mixed-grain fact tables - forces DAX to navigate relationships that shouldn't exist
- Calculated columns in DAX where Power Query could do the work - not compressed the same way imported columns are
Every CALCULATE, FILTER, and RELATED call translates to storage engine operations - segment scans, hash joins, lookups. The model determines how many. Two lines of DAX can generate 47 SE queries if the model requires navigating a bad join.
When to stop tuning DAX and look at the model
- DAX rewrites cut less than 20% off query time
- Server timings show high SE time relative to FE time
- Simple measures are slow across the board - not just one
- Performance drops significantly when row count increases modestly
A well-written measure on a bad model is still slow. A mediocre measure on a clean model is usually fast enough.