Variables inside DAX measures solve a problem you feel before you can name it. A measure that started as three lines grows to twelve, the same sub-expression appears four times, and when a number looks wrong you have to trace which copy of the logic produced it. VAR / RETURN lets you name a piece of logic once, use it many times, and read the measure like a short procedure instead of a nested puzzle.
This article covers the syntax, how variables interact with filter context, the performance implications, and the patterns that show up in production models. It assumes you are comfortable with CALCULATE and basic evaluation context.
The syntax
A DAX measure (or calculated column, or calculated table expression) can contain any number of variable definitions between the measure name and the RETURN keyword:
Total Sales YoY % =
VAR CurrentSales = [Total Sales]
VAR PriorYearSales =
CALCULATE(
[Total Sales],
SAMEPERIODLASTYEAR( 'Date'[Date] )
)
VAR Result =
DIVIDE( CurrentSales - PriorYearSales, PriorYearSales )
RETURN
Result
Three rules govern the syntax:
- A variable name must be unique within the expression and cannot collide with a column or measure name in scope without causing ambiguity. Prefixing with
_orvis a common convention. - Variables are declared in order. A later
VARcan reference an earlier one; the reverse is not allowed. RETURNis mandatory and takes exactly one expression, though that expression can be a table.
Variables are scoped to the expression they are declared in. A variable declared in a measure is not visible to other measures, and a variable declared inside a CALCULATE filter argument is not visible outside it.
Where VAR actually helps
Removing repeated logic
The classic case is a ratio with a guard clause:
Profit Margin % =
VAR Revenue = SUM( Sales[Revenue] )
VAR Cost = SUM( Sales[Cost] )
VAR Profit = Revenue - Cost
RETURN
IF(
Revenue = 0,
BLANK(),
DIVIDE( Profit, Revenue )
)
Without variables you either repeat SUM( Sales[Revenue] ) in three places or nest the subtraction inside DIVIDE. Variables remove both problems.
Conditional branching that stays readable
SWITCH with a variable reads like a lookup table:
Customer Segment =
VAR Spend =
CALCULATE( [Total Sales], ALLEXCEPT( Customer, Customer[CustomerKey] ) )
RETURN
SWITCH(
TRUE(),
Spend >= 100000, "Platinum",
Spend >= 25000, "Gold",
Spend >= 5000, "Silver",
"Bronze"
)
Because Spend is computed once, the four comparisons do not recompute it. The measure is also easier to tune: change the thresholds, not the logic.
Storing a filter context for reuse
CALCULATE modifiers such as KEEPFILTERS or TREATAS benefit from being named:
Sales to Selected Categories =
VAR SelectedCats =
TREATAS( VALUES( 'Category Picker'[Category] ), Product[Category] )
RETURN
CALCULATE( [Total Sales], SelectedCats )
The variable holds a table expression, which is legal because CALCULATE accepts table filters. Naming it documents intent and avoids repeating the TREATAS call inside multiple CALCULATE branches.
Returning two values from one pass
You cannot return a tuple from DAX, but you can compute two aggregates in variables and then pick one based on a parameter:
Dynamic Metric =
VAR SalesAmt = [Total Sales]
VAR MarginPct = [Profit Margin %]
RETURN
SWITCH(
SELECTEDVALUE( 'Metric Selector'[Metric] ),
"Sales", SalesAmt,
"Margin", MarginPct
)
Both aggregates are evaluated when the measure runs, which is usually fine because the storage engine caches the underlying scans. If you have hundreds of selector options, evaluate lazily with nested IF on the selector instead of precomputing every branch.
Filter context and the evaluation trap
This is where most mistakes happen. A variable captures the filter context at the point of declaration, and when you reference it later, it uses that captured value even if CALCULATE has since changed the context.
Wrong Prior Year Comparison =
VAR CurrentSales = [Total Sales]
RETURN
CALCULATE(
CurrentSales,
SAMEPERIODLASTYEAR( 'Date'[Date] )
)
CurrentSales was evaluated in the original filter context, so CALCULATE shifts the date filter but the value is already fixed. The measure returns the current-year number, not the prior-year one. The fix is to declare the variable inside the modified context, or wrap the measure reference directly:
Correct Prior Year Comparison =
VAR PriorSales =
CALCULATE(
[Total Sales],
SAMEPERIODLASTYEAR( 'Date'[Date] )
)
RETURN
DIVIDE( [Total Sales] - PriorSales, PriorSales )
The rule to internalise: variables store values, not expressions. If you need the logic re-evaluated under a new context, call the measure or the aggregation inside that context, not through a variable captured earlier.
This behaviour is intentional and useful. Capturing a total and then comparing it against a filtered result is the foundation of percentage-of-total measures:
% of Grand Total =
VAR GrandTotal =
CALCULATE( [Total Sales], REMOVEFILTERS() )
RETURN
DIVIDE( [Total Sales], GrandTotal )
Here the capture is exactly what you want.
Performance notes
Variables do not automatically make DAX faster, but they remove two common sources of slow measures:
- Repeated scans. Every time you write
SUM( Fact[Amount] )insideCALCULATE, the engine may issue another storage engine query. Variables typically fold these into one. - Repeated context transitions. A
SUMXover a large fact table with an innerCALCULATEper row is expensive; hoisting the invariant piece into a variable cuts the work.
Two caveats. First, a variable that holds a table materialises it in memory for the lifetime of the measure, so do not wrap million-row filtered tables in variables just for tidiness. Second, the engine cannot push variable-based logic into the storage engine as efficiently as a plain aggregate in some cases; if a measure slows down after you refactor to variables, test again with and without. Performance Analyzer shows you the storage engine versus formula engine split, and it will tell you quickly whether the change helped.
Variables are also evaluated once per query, not once per cell. If a measure is used in a hundred visual cells with different filter contexts, each cell gets its own evaluation of the variables, so the cost is per-cell like everything else in DAX.
Naming conventions that survive review
Teams that adopt variables tend to converge on a few habits:
| Habit | Why |
|---|---|
Prefix with _ or a domain word (Sales_, Prior_) | Avoids collisions with column names |
Describe the value, not the calculation (Revenue, not SumA) | Reads correctly at the point of use |
| One variable per logical concept | Makes stepping through in DAX Studio straightforward |
| No side-effect variables | A variable that is declared but never used is dead weight |
Consistency matters more than the specific convention. New joiners read measures; variables are the primary comments in a DAX expression.
Debugging with variables
The practical payoff appears when something is wrong. Instead of guessing which sub-expression is off, you can return the intermediate value:
Debug Margin =
VAR Revenue = SUM( Sales[Revenue] )
VAR Cost = SUM( Sales[Cost] )
VAR Profit = Revenue - Cost
RETURN
-- swap this to Revenue or Cost to inspect them
Profit
Comment out the other variables, return one at a time, and drop the measure into a table visual. This is faster than rebuilding the measure from scratch, and you delete the scaffolding when you are done. The same technique works in DAX Studio’s query pane, where you can wrap a measure in EVALUATE ROW(...) and read every intermediate result side by side.
FAQ
Q: Can variables be declared inside a CALCULATE filter argument?
Yes, and it is common for readability. The variable is scoped to that argument, not the outer expression. It cannot see variables declared after it. If you need the value outside the filter argument, declare it once at the top of the measure and reference it inside CALCULATE.
Q: Do variables improve performance, or is that a myth?
They often help because they eliminate repeated aggregation and repeated context transitions, but they are not free. A variable holding a table materialises it, and the formula engine sometimes works harder with variables than with an equivalent inlined expression. Measure both versions with Performance Analyzer before claiming an improvement.
Q: Why does my variable return the wrong value inside CALCULATE?
Because variables capture their value at declaration time using the filter context that is active there. When CALCULATE later modifies the context, the variable keeps its original value. Move the variable inside the modifier that changes the context, or reference the measure directly inside CALCULATE so it re-evaluates.
Q: Can a variable hold a table, a filter, or a measure reference?
A variable can hold any scalar or table expression: numbers, strings, booleans, a SUMMARIZE result, a TREATAS filter, a VALUES list, even a whole table. A measure reference is not a distinct type; [Total Sales] is an expression that evaluates to a scalar under the current context, and that scalar is what the variable stores.
Q: Are variables available in calculated columns and calculated tables?
Yes. The syntax is identical. In calculated columns, remember that you have row context but no filter context, so a variable holding SUM( Sales[Amount] ) gives the table-wide sum, not a per-row value. Reach for RELATED or CALCULATE with the row’s key when you need something context-dependent.