“How are we doing versus last year?” is the question behind most Power BI reports. Time intelligence in DAX answers it — but only if your data model is set up correctly. This guide covers the setup and the five patterns that cover 95% of date-based reporting.
The non-negotiable: a proper calendar table
Time intelligence functions will silently return wrong results — or blank — if you don’t have a dedicated date table. The requirements:
- One row per day, no gaps, covering the full date range of your fact data (and ideally a buffer on both ends).
- A
Datecolumn of typeDate(notDateTime, not text). - Marked as a date table — in Power BI Desktop, select the table → Table Tools → Mark as Date Table → choose the Date column.
- A relationship from
Date[Date]to your fact table’s date column (one-to-many, single direction).
You can generate this table in Power Query or DAX. The DAX approach is fastest to prototype:
Date =
VAR _Start = DATE(2020, 1, 1)
VAR _End = DATE(2026, 12, 31)
RETURN
ADDCOLUMNS(
CALENDAR(_Start, _End),
"Year", YEAR([Date]),
"Quarter", "Q" & FORMAT([Date], "Q"),
"YearQuarter", FORMAT([Date], "YYYY") & " Q" & FORMAT([Date], "Q"),
"Month", FORMAT([Date], "MMMM"),
"YearMonth", FORMAT([Date], "YYYY-MM"),
"MonthKey", YEAR([Date]) * 100 + MONTH([Date]),
"Weekday", FORMAT([Date], "dddd"),
"IsWeekend", WEEKDAY([Date], 2) > 5
)
If your fiscal year doesn’t start January 1, add a
FiscalYearandFiscalQuartercolumn. Most time intelligence functions accept a fiscal year-end date as the optional third argument.
Pattern 1: Year-to-date (YTD)
Revenue YTD =
TOTALYTD(
[Total Revenue],
Date[Date]
)
TOTALYTD accumulates from January 1 of the current year through the latest date in the current filter context. Pair it with a month-level visual and you’ll see the running total reset each January.
Quarter-to-date and month-to-date work identically:
Revenue QTD = TOTALQTD([Total Revenue], Date[Date])
Revenue MTD = TOTALMTD([Total Revenue], Date[Date])
Pattern 2: Year-over-year (YoY)
Two equivalent approaches — pick whichever reads clearer to you:
// Approach 1: SAMEPERIODLASTYEAR
Revenue LY =
CALCULATE(
[Total Revenue],
SAMEPERIODLASTYEAR(Date[Date])
)
// Approach 2: DATEADD
Revenue LY =
CALCULATE(
[Total Revenue],
DATEADD(Date[Date], -1, YEAR)
)
Then compute the growth rate:
YoY Growth % =
VAR _Current = [Total Revenue]
VAR _Prior = [Revenue LY]
RETURN
DIVIDE(_Current - _Prior, _Prior)
Using variables (VAR) makes the formula readable and is slightly faster — the engine evaluates each variable once.
Pattern 3: Rolling 12-month average
Revenue Rolling 12M =
CALCULATE(
AVERAGEX(
DATESINPERIOD(
Date[Date],
MAX(Date[Date]),
-12,
MONTH
),
[Total Revenue]
)
)
Breaking this down:
MAX(Date[Date])gets the last date visible in the current filter context.DATESINPERIODreturns a 12-month window ending at that date.AVERAGEXiterates over each day in that window and averages the revenue.
A common mistake is using
DATESINPERIODinsideCALCULATEdirectly without the iterator. That returns the sum, not the average — becauseCALCULATEapplies the date range as a filter to the existing measure.
Pattern 4: Same period last year, year-to-date
Combine the two — show last year’s YTD figure next to this year’s, so February 2026 compares to February 2025’s YTD (not the full year 2025):
Revenue YTD LY =
CALCULATE(
[Revenue YTD],
SAMEPERIODLASTYEAR(Date[Date])
)
This is the pattern executives actually want. “How does our YTD through March compare to the same period last year?” — answered by one formula.
Pattern 5: Moving annual total (MAT)
The sum of the trailing 12 months, used to smooth out seasonality:
Revenue MAT =
CALCULATE(
[Total Revenue],
DATESINPERIOD(
Date[Date],
MAX(Date[Date]),
-1,
YEAR
)
)
Distinct from the rolling 12-month average — MAT sums; rolling 12M averages. Choose based on the question you’re answering.
The five most common time intelligence mistakes
- Forgetting to mark the date table. Time intelligence functions return blank or wrong numbers silently. Always verify the table is marked.
- Using
Date[Date]from the fact table instead of the calendar table. Time intelligence requires the calendar table’s date column. - Mixing
TOTALYTDwith manual date filters.TOTALYTDrespects the current filter context, so a slicer onDate[Month] = "February"returns only February YTD — which is just February. UseALL(Date)as the third argument if you want YTD to ignore the month slicer. - Comparing partial periods to full periods. “Revenue LY” for November 2025 returns November 2024 — correct. But “Revenue LY YTD” through November 2025 should return Jan–Nov 2024, not all of 2024. Pattern 4 above handles this.
- Assuming
SAMEPERIODLASTYEARworks without a calendar table. It needs a contiguous date range. Gaps in the calendar produce blank rows.
Reference: time intelligence function cheat sheet
| Function | Returns | Typical use |
|---|---|---|
TOTALYTD | Cumulative YTD value | Annual running totals |
SAMEPERIODLASTYEAR | Date shifted -1 year | YoY comparison |
DATEADD | Date shifted by N intervals | Flexible period shifts (quarters, months) |
DATESINPERIOD | Rolling window | Rolling averages, MAT |
PARALLELPERIOD | Equivalent prior period | QoQ, MoM comparisons |
DATESMTD / DATESQTD / DATESYTD | Date table for MTD/QTD/YTD | Custom time calcs |
What’s next
With these five patterns you can answer nearly every “vs. last year” question. The remaining 5% — fiscal calendars, 4-4-5 retail calendars, week-based time intelligence — requires custom date tables and the DATEADD approach rather than built-in functions. We’ll cover those in the advanced guide.
Pair these time intelligence measures with a well-designed star schema (see Star Schema Data Modeling) and your reports will be both fast and easy to maintain.