Power BI Course
DAX Best Practices
A measure that returns the right number today is not the same as a measure that is still correct, readable, and maintainable in six months when someone else opens the file. DAX best practices are the habits — naming conventions, organisation, documentation, performance rules, and testing discipline — that separate a professional Power BI model from one that no one dares to touch. This lesson covers all of them.
Naming Conventions
Consistent naming makes the Fields pane readable and prevents the most common authoring error: accidentally referencing the wrong field. The conventions below are widely adopted across the Power BI community and align with how the Fields pane sorts and groups items.
| Item type | Convention | Examples |
|---|---|---|
| Measures | Title Case with spaces. No prefix. Descriptive enough to stand alone in any visual's tooltip. | Total Revenue Revenue YoY % Avg Order Value |
| Calculated columns | Title Case. Avoid names identical to measures — the Fields pane shows both and ambiguity causes wrong field drops. | Revenue Tier Customer Full Name Is Late Flag |
| Tables | Singular noun, Title Case. Fact tables descriptive, dimension tables prefixed with "Dim" or left plain. Calendar table always named "Calendar". | Orders Customer Product Calendar |
| Columns | Title Case with spaces. No underscores (these are Power Query habits, not DAX). Remove technical prefixes like "tbl_" or "fld_". | Order Date Customer ID Revenue |
| VAR names inside measures | PascalCase, no spaces. Descriptive. Avoid single letters (x, y, temp) — they are unreadable in complex measures. | CurrentRevenue PriorYearRev GrowthAmount |
| Hidden helper measures | Prefix with underscore _ to sort them to the top (or bottom) of the Fields pane and signal they are internal building blocks, not for direct use in visuals. | _Base Revenue _Prior Period _Rank Helper |
// ❌ Bad naming — ambiguous, inconsistent, hard to maintain
rev = SUM(Orders[revenue])
calc_1 = [rev] - [prev_rev]
x = DIVIDE([calc_1], [prev_rev])
Col1 = Orders[Revenue] * 1.1
// ✓ Good naming — self-documenting, consistent
Total Revenue =
SUM(Orders[Revenue])
Revenue YoY Change =
[Total Revenue] - [Revenue Prior Year]
Revenue YoY % =
DIVIDE([Revenue YoY Change], [Revenue Prior Year])
Adjusted Revenue = -- calculated column
Orders[Revenue] * 1.1
Measure Organisation — Display Folders
Display folders group related measures inside the Fields pane without creating separate tables. A model with 40 measures dumped in one flat list is unusable. The same model with measures grouped by business area — Revenue, Costs, KPIs, Time Intelligence, Helpers — is immediately navigable.
// Set display folders in Power BI Desktop:
// 1. Click a measure in the Fields pane
// 2. In the Measure tools ribbon → Display folder
// 3. Type a folder name — nested folders use backslash: "Revenue\Time"
// Recommended folder structure for a sales model:
//
// 📁 KPIs
// Total Revenue
// Gross Margin %
// Order Count
// Avg Order Value
//
// 📁 Revenue
// 📁 Revenue\YoY
// Revenue Prior Year
// Revenue YoY Change
// Revenue YoY %
// 📁 Revenue\YTD
// Revenue YTD
// Revenue PY YTD
// Revenue YTD vs PY YTD %
//
// 📁 Orders
// Orders Late Count
// Orders Late %
// Avg Days to Deliver
//
// 📁 _Helpers ← underscore sorts helpers to top or bottom
// _Base Revenue
// _Prior Period Rev
// _Rank Helper
Writing Maintainable Measures — The VAR Pattern
Every complex measure should use VAR and RETURN. Variables make the intent of each sub-calculation explicit, prevent the same expression from being evaluated multiple times, and make debugging possible by temporarily changing the RETURN value to any named variable to inspect intermediate results.
// ❌ Hard to read — everything nested, no intermediate names
Revenue YoY % =
DIVIDE(
SUM(Orders[Revenue]) -
CALCULATE(SUM(Orders[Revenue]),
SAMEPERIODLASTYEAR(Calendar[Date])),
CALCULATE(SUM(Orders[Revenue]),
SAMEPERIODLASTYEAR(Calendar[Date]))
)
// SAMEPERIODLASTYEAR evaluated twice — performance cost
// Cannot debug intermediate values without rewriting
// ✓ Maintainable — VAR names each step, RETURN is clear
Revenue YoY % =
VAR CurrentRevenue = SUM(Orders[Revenue])
VAR PriorRevenue =
CALCULATE(
SUM(Orders[Revenue]),
SAMEPERIODLASTYEAR(Calendar[Date])
)
VAR GrowthAmount = CurrentRevenue - PriorRevenue
RETURN
DIVIDE(GrowthAmount, PriorRevenue)
// Debugging tip: temporarily change RETURN to any VAR to inspect it
-- RETURN PriorRevenue ← shows prior year revenue in every cell
-- RETURN GrowthAmount ← shows the raw change amount
| Month | RETURN CurrentRevenue | RETURN PriorRevenue | RETURN GrowthAmount | RETURN final % |
|---|---|---|---|---|
| Jan 2024 | 5,100 | 4,200 | 900 | +21.4% |
| Feb 2024 | 4,600 | 3,800 | 800 | +21.1% |
| Mar 2024 | 6,200 | 5,100 | 1,100 | +21.6% |
Performance Rules
Most Power BI performance problems trace back to a small set of DAX authoring decisions. The rules below come from the VertiPaq engine's design — understanding why each rule exists makes it easier to apply correctly.
| Rule | Why it matters |
|---|---|
| Avoid calculated columns on large fact tables | Every calculated column is stored per row. A text column with 50 distinct values on a 10M-row fact table stores 10M values — even if they compress well. Add dimension columns to dimension tables via RELATED() instead, where they affect far fewer rows. |
| Prefer SUM over SUMX when the column exists | SUM uses native VertiPaq aggregation — it reads compressed column segments directly. SUMX iterates row by row in the formula engine, which is significantly slower on large tables. |
| Use DIVIDE() not / | DIVIDE is optimised for safe division and slightly faster than IF(denominator=0, BLANK(), numerator/denominator) written manually. It also prevents unhandled errors that make visuals blank without explanation. |
| Use VAR to avoid evaluating the same expression twice | If PriorRevenue appears three times in a formula without VAR, DAX evaluates the CALCULATE expression three times. With VAR, it evaluates once and reuses the result. In complex time intelligence measures this is a meaningful performance saving. |
| Avoid FILTER on large tables when a column filter suffices | CALCULATE(SUM([Revenue]), Orders[Region]="North") uses a column filter — fast, processed by VertiPaq. CALCULATE(SUM([Revenue]), FILTER(Orders, [Region]="North")) iterates every row — slower. Use column filters for simple equality conditions. |
| Use ISBLANK() not = BLANK() or = "" | ISBLANK() is specifically optimised for blank detection. Comparing to BLANK() or "" forces DAX to perform a type conversion before comparing, which is slower and can produce unexpected TRUE for zeros. |
Measure Documentation Pattern
DAX does not have a built-in documentation system, but comments inside measures serve the same purpose. A well-documented complex measure pays off every time someone — including your future self — needs to understand or modify it.
// Recommended documentation pattern for complex measures
Revenue YTD vs PY YTD % =
-- PURPOSE: Shows year-to-date revenue growth vs the same YTD
-- point in the prior year. Used in the Executive Summary
-- page header card. Requires Calendar table relationship.
-- DEPENDENCIES: Total Revenue, Calendar[Date]
-- LAST MODIFIED: 2024-03 · added BLANK() guard for Jan prior year
VAR CurrentYTD =
-- YTD from Jan 1 of the current year to the last date in context
CALCULATE(
SUM(Orders[Revenue]),
DATESYTD(Calendar[Date])
)
VAR PriorYTD =
-- YTD from Jan 1 of the prior year to the same date one year ago
CALCULATE(
SUM(Orders[Revenue]),
DATESYTD(SAMEPERIODLASTYEAR(Calendar[Date]))
)
VAR GrowthAmount = CurrentYTD - PriorYTD
RETURN
-- Guard: return BLANK if prior year has no data (e.g. new data set)
IF(
ISBLANK(PriorYTD),
BLANK(),
DIVIDE(GrowthAmount, PriorYTD)
)
Testing DAX Measures
The most reliable way to test a measure is to validate it in a table visual with known data at multiple levels of granularity. A card showing a grand total proves nothing about whether the measure handles filter context correctly. A table showing results per region, per month, and at the total row exposes the majority of context errors.
// Testing checklist — validate every new measure against all five:
// 1. Grand total (no slicer)
// Expected: sum of all data
// Fails if: ALLSELECTED used where ALL was intended
// 2. Single dimension slice (e.g. Region = North in slicer)
// Expected: filtered total for that dimension
// Fails if: CALCULATE replaces filter instead of intersecting
// 3. Table visual with dimension on rows
// Expected: each row = correct value for that dimension
// Fails if: row context not converting to filter context correctly
// 4. Total row in the table visual
// Expected: grand total, not sum of visible row values
// Common failure: RANKX returning 1 for the total row;
// % of total returning more than 100%
// 5. BLANK handling — filter to a dimension value with no data
// Expected: BLANK() or a designated fallback, not 0 or error
// Fails if: DIVIDE denominator is 0, or SAMEPERIODLASTYEAR
// has no prior year data
// Debugging workflow:
// Step 1: Add the measure to a table visual with all dimensions
// Step 2: Check grand total row first
// Step 3: Check individual dimension rows
// Step 4: Apply a slicer and repeat steps 2-3
// Step 5: If wrong, change RETURN to each VAR to find the failure point
| Test scenario | Expected | Actual | Pass? |
|---|---|---|---|
| Grand total, no filter | +21.4% | +21.4% | ✓ |
| Slicer = North only | +18.2% | +18.2% | ✓ |
| Table rows = Month (Jan–Mar 2024) | +21.4%, +21.1%, +21.6% | matches | ✓ |
| Total row of table visual | +21.4% (Q1 total) | +21.4% | ✓ |
| Filter to period with no prior year | BLANK (not 0) | BLANK | ✓ |
Model Hygiene Checklist
Teacher's Note: The single best practice that costs the least and returns the most is: always use VAR and RETURN in any measure longer than two lines. When a measure breaks — and they do break, when data changes, when a column gets renamed, when a new region appears with no prior year — you can debug it in thirty seconds by changing the RETURN value to each VAR one at a time and watching which one goes wrong. Without VAR, debugging means reading a wall of nested functions and guessing which sub-expression is failing. The habit of writing VAR takes five extra seconds. The habit of not writing it costs five hours.
Practice
Practice 1 of 3
Helper measures that are building blocks for other measures but should not be used directly in visuals are conventionally prefixed with ___ so they sort to the top of the Fields pane and signal their internal-only purpose.
Practice 2 of 3
To organise the Fields pane by grouping related measures without creating extra tables, you assign measures to ___ folders from the Measure tools ribbon — supporting nested sub-folders using a backslash separator like "Revenue\YoY".
Practice 3 of 3
When testing a new measure, checking only a card visual showing the grand total is insufficient. You should also validate it in a ___ visual with a dimension on rows to confirm the measure handles row-level filter context correctly across all dimension values.
Lesson Quiz
Quiz 1 of 3
A measure uses SAMEPERIODLASTYEAR and the same sub-expression appears three times in the formula without VAR. A colleague says this is fine because DAX caches expression results. Is the colleague correct?
Quiz 2 of 3
A report uses a FILTER(Orders, [Region]="North") inside CALCULATE instead of the simpler column filter Orders[Region]="North". Both return the same result. Why should you prefer the column filter form?
Quiz 3 of 3
A Revenue YoY % measure returns +21.4% in the grand total card and +21.4% in every month row of a table visual — including for months with no prior year data where BLANK is expected. What is the most likely cause?
Next up — Lesson 41 begins Section IV and introduces the full range of Power BI visualisation types, when to use each one, and the visual design principles that make data communicate clearly rather than just display numbers.