Why this matters
As a BI Developer, you will constantly explain performance: Are we up or down? By how much? Compared to what? Variance and percent change are the backbone of KPI reporting in executive dashboards, product analytics, and finance reports.
- Compare Actual vs Target for revenue, costs, or operational KPIs.
- Show Month-over-Month (MoM) and Year-over-Year (YoY) trends.
- Flag anomalies with conditional formatting based on percent changes.
- Enable drilldowns that keep logic consistent from product to total.
Tip: Your quick test is available to everyone; sign in to save your progress automatically.
Concept explained simply
Variance answers “How much did we move?” Percent change answers “How fast did we move?”
- Variance = Current - Comparison
- Percent change = (Current - Comparison) / Comparison
In DAX, the key is to compute the comparison value by changing filter context (time, scenario, etc.) and then calculate the difference safely (avoid divide-by-zero, handle blanks).
Mental model
- Start with a base measure (e.g., [Sales]).
- Use CALCULATE to get the comparison (previous period, target, last year).
- Use DIVIDE(numerator, denominator, alternateResult) to avoid errors when denominator is zero or blank.
- Return BLANK() when comparison is missing; it keeps visuals clean.
- Use variables (VAR) for readability and performance.
Core DAX patterns
Variance to Target
-- Base measures
a) Sales := SUM ( Sales[SalesAmount] )
b) Target := SUM ( Targets[TargetAmount] )
-- Variance to Target
Variance to Target := [Sales] - [Target]
-- Variance % to Target
Variance % to Target :=
VAR Curr = [Sales]
VAR Tgt = [Target]
RETURN DIVIDE( Curr - Tgt, Tgt )Format: Variance as Whole/Decimal; Variance % as Percentage with 1–2 decimals.
Month-over-Month (MoM) % change
-- Requires a proper Date table marked as Date table
Previous Month Sales :=
CALCULATE( [Sales], DATEADD( 'Date'[Date], -1, MONTH ) )
MoM % Sales :=
VAR Curr = [Sales]
VAR Prev = [Previous Month Sales]
RETURN DIVIDE( Curr - Prev, Prev )Works at any granularity as long as a single month is in context (or totals aggregate across months).
Year-over-Year (YoY) % change
Previous Year Sales :=
CALCULATE( [Sales], SAMEPERIODLASTYEAR( 'Date'[Date] ) )
YoY % Sales :=
VAR Curr = [Sales]
VAR Prev = [Previous Year Sales]
RETURN DIVIDE( Curr - Prev, Prev )For fiscal calendars, replace with parallelperiod on your fiscal month column.
Worked examples
1) Actual vs Target (card + matrix)
Goal: Show Variance and Variance % for current selection.
Variance to Target := [Sales] - [Target]
Variance % to Target :=
VAR Curr = [Sales]
VAR Tgt = [Target]
RETURN DIVIDE( Curr - Tgt, Tgt )- Card visuals for Variance and Variance %.
- Matrix by Product shows contribution; total row shows overall variance.
- If a product has no target, the % returns blank to avoid noise.
2) MoM % on a line chart
Previous Month Sales := CALCULATE( [Sales], DATEADD( 'Date'[Date], -1, MONTH ) )
MoM % Sales :=
VAR Curr = [Sales]
VAR Prev = [Previous Month Sales]
RETURN DIVIDE( Curr - Prev, Prev )- Use Month on axis, MoM % as values.
- First month shows blank (no previous month).
- Totals represent the weighted change across months in the filter context.
3) YoY % with slicers
Previous Year Sales := CALCULATE( [Sales], SAMEPERIODLASTYEAR( 'Date'[Date] ) )
YoY % Sales :=
VAR Curr = [Sales]
VAR Prev = [Previous Year Sales]
RETURN DIVIDE( Curr - Prev, Prev )- Use a Year slicer; measure respects filters like Region and Product.
- For partial-year selections, the comparison aligns to the same dates last year.
4) Safe totals with ISINSCOPE (optional)
Variance % Safe :=
VAR Curr = [Sales]
VAR Comp = [Previous Year Sales]
RETURN
IF(
ISINSCOPE( 'Product'[Product] ),
DIVIDE( Curr - Comp, Comp ),
-- At higher levels, recompute explicitly for clarity
DIVIDE( Curr - Comp, Comp )
)Often the plain formula is fine; this pattern is useful when totals look unexpected due to different weighting at aggregate levels.
Who this is for
- BI Developers building KPI dashboards and variance analysis.
- Data analysts transitioning to DAX-based modeling.
- Analytics engineers who need consistent time comparisons.
Prerequisites
- A star schema with a proper Date table marked as the date table.
- Basic DAX measures (SUM, CALCULATE) and filter context understanding.
- Familiarity with your business’s KPI definitions (Actual, Target, Budget).
Learning path
- Set up base measures ([Sales], [Target]).
- Build comparison measures (previous month/year; targets).
- Add variance and percent change with DIVIDE.
- Test at different granularities (day, month, product, total).
- Format, color-code, and document assumptions.
Edge cases and formatting
- Divide-by-zero: Always use DIVIDE to avoid errors and return BLANK().
- Missing comparison period: Expect BLANK() for first month or new products.
- Targets not aligned to date grain: Aggregate targets to match the visual’s context.
- Sign conventions: Decide whether a negative variance is good (e.g., lower cost). Document it.
- Percent formats: Use percentage with 1–2 decimals; avoid more unless needed.
Exercises you will build now
Dataset assumption (typical model):
- Fact table: Sales(Date, Product, Segment, Quantity, SalesAmount)
- Targets table: Targets(Date, Product, TargetAmount)
- Date table: 'Date' with continuous dates; marked as Date table
Replicate the following exercises. When done, compare with the solutions in each exercise card below.
- Create and validate base measures: [Sales], [Target].
- Build Variance and Variance % to target.
- Compute MoM % and YoY %.
- Check results at Product and Total levels.
Exercise 1 — Actual vs Target variance
Create:
- [Sales] = SUM(Sales[SalesAmount])
- [Target] = SUM(Targets[TargetAmount])
- [Variance to Target] and [Variance % to Target]
Validate on visuals: Card for Variance; Card for Variance %; Matrix by Product.
- Expected behavior: Numbers add up at total; Variance % is blank where Target is blank or zero; formatting is clear.
Exercise 2 — MoM and YoY % change
Create:
- [Previous Month Sales] with DATEADD -1 month
- [MoM % Sales] using DIVIDE
- [Previous Year Sales] with SAMEPERIODLASTYEAR
- [YoY % Sales] using DIVIDE
Validate on visuals: Line chart by Month; Matrix by Year and Month.
- Expected behavior: First month shows blank MoM; first year shows blank YoY; respects filters (Region, Segment).
Common mistakes and self-check
- Forgetting a proper Date table: Time-intelligence (DATEADD/SAMEPERIODLASTYEAR) won’t work reliably. Self-check: Is your Date table marked as the Date table?
- Using direct division: Risks divide-by-zero errors or misleading infinities. Self-check: Did you use DIVIDE?
- Comparing across mismatched grains: Daily sales vs monthly targets. Self-check: Do the measures aggregate the same grain in the current filter context?
- Hard-coding filters in measures: Breaks slicers. Self-check: Do visuals still respond to slicers after you add CALCULATE?
- Ambiguous totals: Totals not matching expectations. Self-check: Recreate total logic with explicit vars to understand weighting.
Practical projects
- Executive KPI board: Cards for Actual, Target, Variance, Variance %, with MoM and YoY on sparklines.
- Category drilldown: Category → Subcategory → Product variance analysis with conditional formatting.
- Alerting-ready measure pack: Measures that return BLANK() for noisy edge cases and consistent signs for alerts.
Next steps
- Add rolling windows (Last 3 Months, Last 12 Months) for smoother comparisons.
- Support Budget vs Forecast vs Actual with a Scenario dimension.
- Document business rules (good/bad direction, rounding) in your model notes.
Mini challenge
Create a Rolling 3-Month % Change vs the previous 3 months:
- Make [Sales L3M] = sales for the last 3 complete months.
- Make [Sales Prev L3M] = the 3 months before that.
- Return DIVIDE([Sales L3M] - [Sales Prev L3M], [Sales Prev L3M]).
Hint
Use DATESINPERIOD for L3M and shift it by -3 months for the previous block.
Suggested solution
Sales L3M :=
CALCULATE( [Sales], DATESINPERIOD( 'Date'[Date], MAX('Date'[Date]), -3, MONTH ) )
Sales Prev L3M :=
VAR EndDate = MAX( 'Date'[Date] )
VAR PrevEnd = EOMONTH( EndDate, -3 )
RETURN
CALCULATE( [Sales], DATESINPERIOD( 'Date'[Date], PrevEnd, -3, MONTH ) )
L3M % Change :=
VAR Curr = [Sales L3M]
VAR Prev = [Sales Prev L3M]
RETURN DIVIDE( Curr - Prev, Prev )Ready? Quick Test
Take the quick test below. Everyone can take it for free; sign in to save your progress.