Skip to main content

DAX & Semantic Model Standards

Standards for DAX measure documentation, naming conventions, formatting, and semantic model organization. This is a companion to the Git & Azure DevOps Guide — that document covers the development workflow; this one covers what good Power BI code looks like.


Table of Contents

  1. Why This Matters
  2. Measure Documentation
  3. Measure Naming Conventions
  4. DAX Formatting & Style
  5. Display Folders
  6. Semantic Model Hygiene
  7. Code Review Checklist for DAX

1. Why This Matters

These standards exist for three reasons:

  • Handoff and continuity. When someone else picks up your report — or you come back to it six months later — undocumented measures with cryptic names are a time sink. Clear names and descriptions eliminate guesswork.
  • Code review quality. We are introducing peer review for all new DAX measures. Reviewers need to be able to read and understand your logic quickly. Consistent formatting and commenting makes that possible.
  • Automated validation. We plan to add a Claude agent that scans PRs for DAX best practices. These standards define what "best practices" means for our team so the automation has clear rules to enforce.

2. Measure Documentation

Every DAX measure must be documented. No exceptions.

Measure description (required for every measure)

The description field is visible in Power BI tooltips, the field list, and to anyone browsing the semantic model. Write it as a short sentence that a business user could understand.

In Power BI Desktop: Right-click the measure → Properties → Description field.

In TMDL files (when editing directly): The description is a property on the measure object.

✅ "Total occupied units divided by total available units, expressed as a percentage."
✅ "Year-to-date count of new move-ins, using fiscal year starting April 1."
✅ "Variance between actual and budgeted revenue. Positive = over budget."

❌ "Calculates occupancy" ← too vague
❌ "" ← empty — never acceptable
❌ "DIVIDE(SUM(...), SUM(...))" ← just restates the formula

Rules for descriptions:

  • Write for a business user, not a developer
  • State what the measure calculates and any important context (time frame, filter behavior, what positive/negative means)
  • If the measure behaves differently depending on filter context, say so

Inline DAX comments (required for non-trivial measures)

For any measure longer than a single line, or any measure with business logic that is not immediately obvious, add comments explaining why the calculation works the way it does. Comments explain the reasoning — the code already shows the what. Claude is a great tool for generating these Inline comments, Give this document to Claude and ask it to remember you want to add these comments to all future measures.

Occupancy Rate =
// Occupied units as a percentage of total available units.
// Uses CALCULATE to override any page-level property type filter
// so the denominator always reflects the full portfolio.
VAR _OccupiedUnits =
SUM( Units[Occupied] )
VAR _TotalUnits =
CALCULATE(
SUM( Units[Total] ),
ALL( Units[Property Type] ) // Remove property type filter for denominator
)
RETURN
DIVIDE( _OccupiedUnits, _TotalUnits, 0 ) // 0 if no units exist

When comments are required:

  • Any use of CALCULATE with filter modification (ALL, REMOVEFILTERS, KEEPFILTERS, USERELATIONSHIP)
  • Time intelligence logic (SAMEPERIODLASTYEAR, DATEADD, DATESYTD, etc.)
  • Business rules that are not obvious from the formula (fiscal year start dates, custom period definitions, specific exclusion logic)
  • Any workaround or non-obvious pattern

When comments are optional:

  • Simple aggregations: Total Revenue = SUM( Sales[Revenue] )
  • Single-line measures where the name and description fully explain the logic

3. Measure Naming Conventions

ConventionExample
Use plain business languageTotal Revenue, Occupancy Rate, YTD Move-Ins
Avoid abbreviations unless universally understoodYTD ✅, Occ Rt
Time-intelligence suffixesRevenue YTD, Revenue PY, Revenue YoY %
Percentage measures end with %Occupancy Rate %, Profit Margin %
Avoid technical prefixesTotal Sales ✅, msr_TotalSales
Do not conflict with DAX function namesTotal Sales ✅, Sum

Standard time-intelligence suffixes

Use these consistently across all reports:

SuffixMeaningExample
YTDYear-to-dateRevenue YTD
PYPrior year (same period)Revenue PY
YoYYear-over-year changeRevenue YoY
YoY %Year-over-year change as percentageRevenue YoY %
MTDMonth-to-dateRevenue MTD
QTDQuarter-to-dateRevenue QTD
LMLast monthRevenue LM
Rolling 12Rolling 12-month totalRevenue Rolling 12

Table and column naming

ConventionExample
Table names use plain nouns with spacesSales, Units, Property, Calendar
No technical prefixes on user-facing tablesSales ✅, fctSales ❌, dim_Property
Hidden/internal tables may use PascalCasePricingConfiguration (if hidden from report users)
Column names use plain language with spacesProperty Type, Move In Date, Unit Count

4. DAX Formatting & Style

Variable naming

Use PascalCase with an underscore prefix to distinguish variables from table columns and measures:

VAR _TotalSales = SUM( Sales[Amount] )
VAR _PreviousYearSales = CALCULATE( [Total Sales], SAMEPERIODLASTYEAR( Calendar[Date] ) )
VAR _YoYChange = _TotalSales - _PreviousYearSales
RETURN
DIVIDE( _YoYChange, _PreviousYearSales, 0 )

Why the underscore prefix? It makes variables instantly distinguishable from measures and columns in complex formulas. When you see _TotalSales you know it is a local variable; when you see [Total Sales] you know it is a measure reference.

Use variables for readability

Break complex measures into named steps. Even if a variable is only used once, it makes the logic easier to follow and debug. When Copying and pasting logic make sure to update variable names to remain relevant.

// ❌ Hard to read — nested functions with no explanation
Net Absorption =
CALCULATE( SUM( Activity[Units] ), Activity[Type] = "Move-In" ) -
CALCULATE( SUM( Activity[Units] ), Activity[Type] = "Move-Out" )

// ✅ Clear — each step is named
Net Absorption =
VAR _MoveIns =
CALCULATE( SUM( Activity[Units] ), Activity[Type] = "Move-In" )
VAR _MoveOuts =
CALCULATE( SUM( Activity[Units] ), Activity[Type] = "Move-Out" )
RETURN
_MoveIns - _MoveOuts

Division: always use DIVIDE

Never use the / operator for division. Use DIVIDE() with an alternate result to handle division by zero gracefully.

// ❌ Will error or return Infinity on zero denominator
Occupancy Rate % = SUM( Units[Occupied] ) / SUM( Units[Total] )

// ✅ Returns 0 (or BLANK) when denominator is zero
Occupancy Rate % = DIVIDE( SUM( Units[Occupied] ), SUM( Units[Total] ), 0 )

Indentation and line breaks

  • One argument per line for functions with multiple arguments
  • Indent nested functions
  • Align RETURN at the same level as VAR
Revenue YoY % =
VAR _CurrentYear =
SUM( Sales[Revenue] )
VAR _PriorYear =
CALCULATE(
SUM( Sales[Revenue] ),
SAMEPERIODLASTYEAR( Calendar[Date] )
)
RETURN
DIVIDE(
_CurrentYear - _PriorYear,
_PriorYear,
BLANK()
)

5. Display Folders

Group related measures into display folders so the field list is navigable. Without folders, a model with 50+ measures becomes unmanageable.

Set display folders in Power BI Desktop: Select the measure → Properties → Display Folder.

Folder structure pattern

Organize by business domain, not by technical type:

📁 Revenue
Total Revenue
Revenue YTD
Revenue PY
Revenue YoY %

📁 Occupancy
Occupancy Rate %
Total Units
Occupied Units
Vacant Units

📁 Move Activity
YTD Move-Ins
YTD Move-Outs
Net Absorption

📁 Budget
Budget Revenue
Budget Variance
Budget Variance %

Rules:

  • Every measure must be in a display folder — no orphan measures floating at the root
  • Folder names use plain business language (match how the client talks about the data)
  • Helper measures (intermediate calculations not shown on reports) go in a 📁 _Utility folder and are hidden from the report view

6. Semantic Model Hygiene

These are general model health practices that make the semantic model easier to maintain and review.

Hide what users don't need

  • Hide foreign key columns (e.g., PropertyID on the fact table) — users should navigate via relationships, not key columns
  • Hide intermediate/staging tables that are only used for calculations
  • Hide columns that exist only for relationships or internal logic

Prefer measures over calculated columns

Use measures for any value that should aggregate or respond to filter context. Calculated columns consume memory and recalculate on refresh — measures calculate on demand.

Use a calculated column when:

  • You need to sort or filter by the value
  • The value depends only on the current row (row context)

Use a measure when:

  • The value should change based on slicers, filters, or visual context
  • You are aggregating, counting, or calculating ratios

Relationships

  • Use single-direction relationships wherever possible
  • Avoid bidirectional filtering unless there is a clear and documented reason
  • If bidirectional is required, add a comment in the relationship's table TMDL file explaining why

7. Code Review Checklist for DAX

When reviewing a PR that contains new or modified measures, check the following:

CheckPass criteria
Description filled inEvery new measure has a non-empty, business-readable description
Inline commentsNon-trivial measures explain why, not just what
Naming conventionPlain business language, standard time-intelligence suffixes, no technical prefixes
Display folder assignedMeasure is in an appropriate folder, not floating at root
DIVIDE used for divisionNo raw / operators — DIVIDE() with alternate result
Variables used for clarityComplex logic is broken into named VAR steps
Filter context correctCALCULATE modifiers (ALL, REMOVEFILTERS, etc.) are intentional and commented
Error handlingEdge cases handled (empty tables, zero denominators, BLANK values)
No FILTER on fact tablesUse CALCULATE with column predicates instead of FILTER( FactTable, ... ) where possible
Hidden appropriatelyHelper/utility measures are hidden from report view