Module 10 · Power BI DAX
Performance and testing
Find slow visuals with Performance Analyzer, rewrite the DAX patterns that cause them, and test measures against the source data before anyone else sees them.
About 25 minutes
The problem
Kolanut's report has grown to five pages and forty measures. The customer page takes eight seconds to load, and the sales director has started exporting to Excel instead. Worse, last week a measure showed ₦76.6m for supermarket sales in one visual and ₦78.0m in another, and nobody could say which was right.
A report that's slow doesn't get used, and a report that's wrong does damage. On 4,266 rows almost anything is fast, but the habits in this lesson are what keep a model fast at 40 million rows, and the testing routine is what lets you say "this number is right" with confidence.
The concept
Find the slow part first
Optimize → Performance Analyzer → Start recording, then refresh the visuals. Each visual's time is split into:
- DAX query: the time the engine spent calculating. This is the part your measures control.
- Visual display: drawing. Too many points, or too many visuals on one page.
- Other: waiting for other visuals.
Copy a slow visual's query into DAX query view to run and change it on its own.
DAX habits that keep measures fast
| Slow | Faster | Why |
|---|---|---|
CALCULATE ( [Revenue], FILTER ( orders, RELATED ( customers[channel] ) = "Wholesale" ) ) | CALCULATE ( [Revenue], customers[channel] = "Wholesale" ) | filter a column, not a whole fact table |
| the same sub-expression written twice | a VAR | calculated once |
IFERROR ( a / b, 0 ) | DIVIDE ( a, b, 0 ) | no error handling needed |
| a calculated column on the fact table that's only ever summed | an iterator in a measure | no stored column eating memory |
| bidirectional relationships "to make it work" | single direction plus explicit DAX | ambiguous paths and slower queries |
[Revenue] + 0 across big tables | leave blanks blank | visuals stay small |
The model matters as much as the DAX. Remove columns nobody uses, and reduce the number of distinct values (split a date-time into a date and a time; round long decimals). The engine compresses columns, and fewer distinct values compress far better.
Testing measures
Before a report goes out, test each important measure:
- Reconcile with the source. Pick a total and check it outside Power BI: in SQL, Excel or the source system.
- Test at every level. Check a row, a subtotal and the grand total. Ratios and distinct counts often don't add up across rows, and they shouldn't. Make sure the total means what the reader will think it means.
- Test the edges. A period with no sales, a customer with one order, a filter that leaves nothing.
- Compare two routes to the same number.
Gross Revenue − Discount Amountshould equalRevenue.
DAX query view makes these checks fast:
DAX
EVALUATE
ROW (
"Order lines", COUNTROWS ( orders ),
"Revenue", [Revenue],
"Check", [Gross Revenue] - [Discount Amount] - [Revenue]
)Check should be 0.
Example
The ₦76.6m versus ₦78.0m mystery. One visual used:
DAX
Supermarket Revenue = CALCULATE ( [Revenue], customers[channel] = "Supermarket" )The other used a copy of the formula pasted months earlier, before discounts were part of revenue:
DAX
Supermarket Revenue (old) =
CALCULATE (
SUMX ( orders, orders[quantity] * orders[unit_price] ),
FILTER ( orders, RELATED ( customers[channel] ) = "Supermarket" )
)The old version is wrong (it ignores discounts) and slow (it filters the whole orders table row by row). For January to June 2026, a SQL query on the source gives ₦76,556,135 of supermarket revenue after discounts, which reconciles with the first measure. The fix is the same as in lesson 1: one base measure, reused, and no pasted copies.
Walkthrough
- Open Performance Analyzer, start recording and click Refresh visuals. Expand the slowest visual and note its DAX query time.
- Click Copy query on that visual, paste it into DAX query view and run it.
- Run the
EVALUATE ROWtest from the concept section. Check thatOrder linesis 4,266 andCheckis 0. - Run a reconciliation query for supermarket revenue in January to June 2026:
DAX
EVALUATE
SUMMARIZECOLUMNS (
customers[channel],
TREATAS ( { 2026 }, 'Date'[Year] ),
"Revenue", [Revenue]
)- Search your measures for
FILTER ( ordersandIFERROR, and rewrite each one using the table above.
Practice
Practice
Run the reconciliation query in step 4. What is Supermarket revenue for 2026? (A rounded figure is fine.)
Practice
How much revenue did the old measure overstate supermarket sales by in 2026, because it ignored discounts? (Gross minus net for Supermarket, 2026. A rounded figure is fine.)
Task
5 minRewrite this slow measure so it filters a column instead of the whole orders table, and builds on the base measure:
DAX
North Revenue = CALCULATE(SUMX(orders, orders[quantity] * orders[unit_price] * (1 - orders[discount_pct] / 100)), FILTER(orders, RELATED(customers[region]) = "North Central" || RELATED(customers[region]) = "North West"))Paste your version.
Your work is checked for
- Named North Revenue
- Uses the [Revenue] base measure
- Filters the customers[region] column directly
- No FILTER over the orders table
- No RELATED
More practice
Drill · optional
Run EVALUATE ROW("Lines", COUNTROWS(orders), "Customers", DISTINCTCOUNT(orders[customer_id]), "Units", [Units]). What is Units?
Check your understanding
Answer every question to check.