Power BI Lesson 40 – DAX Best Practices | Dataplexa
DAX · Lesson 40

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
Fields pane — bad naming vs good naming
❌ Bad — Fields pane looks like this
📋 Orders
Σ calc_1
Σ rev
Σ x
ABC Col1
ABC revenue
Which measure is "% change"? What does "x" do?
✓ Good — Fields pane looks like this
📋 Orders
Σ _Base Revenue
Σ Adjusted Revenue (col)
Σ Revenue YoY %
Σ Revenue YoY Change
Σ Total Revenue
Every field tells you exactly what it is · _ prefix hides helper

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
Fields pane — with display folders applied
📋 Orders
📁 _Helpers
Σ _Base Revenue
Σ _Prior Period Rev
📁 KPIs
Σ Avg Order Value
Σ Gross Margin %
Σ Order Count
Σ Total Revenue
📁 Revenue
📁 YoY
Σ Revenue Prior Year
Σ Revenue YoY %
Σ Revenue YoY Change
📁 YTD
Σ Revenue PY YTD
Σ Revenue YTD
Σ Revenue YTD vs PY YTD %

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
VAR debugging — changing RETURN to inspect intermediate values
Month RETURN CurrentRevenue RETURN PriorRevenue RETURN GrowthAmount RETURN final %
Jan 20245,1004,200900+21.4%
Feb 20244,6003,800800+21.1%
Mar 20246,2005,1001,100+21.6%
Each VAR becomes a debuggable checkpoint · temporarily changing RETURN is faster than adding new measures for debugging

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)
    )
Documented measure — what each section communicates to future readers
Purpose comment
Explains what the measure does and where it is used — prevents accidental deletion or modification by someone who does not recognise it
Dependencies
Lists what other measures and tables must exist — makes it safe to refactor or rename dependencies without breaking this measure silently
Last modified note
Tracks what changed and why — invaluable for bug reports ("this worked last month") and audit trails
Inline VAR comments
Each variable explains what it represents — the reader does not need to trace DAX syntax to understand the intent of CurrentYTD vs PriorYTD
RETURN guard comment
Explains why the ISBLANK check exists — without it a future developer might remove it thinking it is redundant

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
Testing matrix — Revenue YoY % across five validation scenarios
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 yearBLANK (not 0)BLANK
All five scenarios pass · the ISBLANK(PriorYTD) guard in the measure correctly returns BLANK when no prior year data exists

Model Hygiene Checklist

Disable Load on staging queries. Queries like RawOrders and CleanOrders that feed other queries but are not needed in the model should have "Enable Load" unchecked. They remain available for downstream queries but do not add tables to the model.
Hide technical columns from report view. Relationship key columns (like OrderID, CustomerID), helper columns used only by other columns, and surrogate keys should be hidden so report authors cannot accidentally drop them into visuals.
Set sort-by-column for text fields. MonthName should sort by MonthNumber, DayName by DayOfWeek number, Quarter by QuarterNumber. Set this in Model View → Column tools → Sort by column. Without it, alphabetical sort puts August before February.
Set measure format strings. Every measure should have a format string set in Model View — "$#,##0" for currency, "0.0%" for percentages, "#,##0" for integers. Do not rely on visual-level formatting because each visual must be configured separately and will drift over time.
Mark the Calendar table as a Date Table. In Model View → right-click Calendar → Mark as date table → select the Date column. This unlocks the full suite of time intelligence functions and validates that the Calendar has no date gaps.
Delete unused measures and columns. Every unused stored column adds to the file size and refresh time. Every unused measure adds noise to the Fields pane. Clean the model before publishing or sharing.

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.