Why this matters
Time intelligence is how BI answers business questions like: Are we growing? How does this month compare to last year? What is our year-to-date performance? As a BI Developer, you will build measures that respect filters, calendars, and fiscal rules while staying performant and accurate.
- Create dashboards with Year-over-Year (YoY), Month-to-Date (MTD), Quarter-to-Date (QTD), and Year-to-Date (YTD) metrics
- Handle rolling windows (e.g., last 12 months) for trend analysis
- Support fiscal calendars and incomplete periods safely
Concept explained simply
Time intelligence in DAX shifts the current filter context in time. Instead of manually filtering dates, you apply functions that move the time window relative to the current selection. You still aggregate the same measure (like Sales), but over a different time slice (last year, last month, YTD, etc.).
Mental model
- Start with a base measure: e.g., Sales Amount = SUM(Sales[Amount])
- Use CALCULATE to change the filter context
- Apply a time function to the Dates table to define the new time window
- Aggregate again in the modified context
Think: Base measure + CALCULATE + Time window over a proper Date table.
Prerequisites
- You have a Date table with a continuous range and no gaps
- Date table is marked as a Date table and related to fact tables via a single active relationship
- You have at least one base aggregation measure (e.g., Sales Amount)
Date table requirements (checklist)
- [ ] One row per day, no gaps
- [ ] Column of type Date (e.g., 'Date'[Date])
- [ ] Marked as Date table
- [ ] Calendar attributes: Year, Month, Quarter; add Fiscal if needed
- [ ] Single active relationship to facts
How to validate your Date table quickly
- Create a visual with MIN('Date'[Date]) and MAX('Date'[Date]); ranges should match your data horizon
- CountRows of Date table equals number of days in the range
- No duplicates in 'Date'[Date]
Core functions and when to use
- SAMEPERIODLASTYEAR('Date'[Date]): Shifts current selection by -1 year. Good for YoY at day/month/quarter levels.
- DATEADD('Date'[Date], -1, YEAR|MONTH|QUARTER): Flexible shift by a fixed interval; helpful when SAMEPERIODLASTYEAR doesn't match fiscal or custom cycles.
- PARALLELPERIOD('Date'[Date], -1, YEAR): Similar to DATEADD for contiguous month/quarter/year selections; less flexible than DATEADD.
- DATESYTD('Date'[Date], [year_end]), DATESQTD, DATESMTD: Return ranges for YTD/QTD/MTD windows; combine with CALCULATE.
- TOTALYTD([Measure], 'Date'[Date], [year_end]) (and TOTALQTD/TOTALMTD): Shortcut aggregators for running totals.
- DATESINPERIOD('Date'[Date], EndDate, -N, MONTH|DAY|YEAR): Rolling windows like last 12 months.
Note on CALCULATE and filter context
CALCULATE evaluates the expression under modified filters. Time functions return a table of dates; CALCULATE applies it, replacing the existing date filter context.
Worked examples
1) Year-over-Year Sales
Sales Amount = SUM(Sales[Amount])
Sales Amount YoY =
CALCULATE([Sales Amount], SAMEPERIODLASTYEAR('Date'[Date]))Place Month and Year in a matrix. Sales Amount YoY shows last year's value aligned with the current month.
Alternative with DATEADD
Sales Amount YoY (DATEADD) =
CALCULATE([Sales Amount], DATEADD('Date'[Date], -1, YEAR))2) Month-to-Date (MTD) Sales
Sales Amount MTD =
CALCULATE([Sales Amount], DATESMTD('Date'[Date]))MTD respects the last visible date. If your report is filtered to an incomplete month, the measure stops at the latest available date in that month.
Safe comparison: MTD vs previous MTD
Sales Amount MTD Prev Year =
CALCULATE([Sales Amount],
DATESMTD(DATEADD('Date'[Date], -1, YEAR)))3) Rolling 12 Months (R12)
Sales Amount R12 =
VAR EndDate = MAX('Date'[Date])
RETURN CALCULATE(
[Sales Amount],
DATESINPERIOD('Date'[Date], EndDate, -12, MONTH)
)This sums the last 12 months ending at the current context's last date. It stabilizes only after 12 months of data exist.
4) Fiscal Year-to-Date (FY YTD)
Assume fiscal year ends on March 31. Pass year-end as "3/31" (month/day).
Sales Amount FYTD =
TOTALYTD([Sales Amount], 'Date'[Date], "3/31")Equivalent using DATESYTD
Sales Amount FYTD (explicit) =
CALCULATE([Sales Amount], DATESYTD('Date'[Date], "3/31"))Common mistakes and self-check
- Using an unmarked or gappy date table: Time functions may return unexpected results. Self-check: Count rows per year; ensure every day exists.
- Filtering facts instead of the Date table: Avoid filtering Sales[OrderDate] directly in time measures; filter 'Date'[Date] via time functions.
- Confusing DATEADD with arithmetic on dates: Do not do 'Date'[Date] - 365; it ignores calendar logic and leap years.
- Ignoring incomplete periods: MTD/QTD/YTD respect the last date in filter context, not “today” unless you filter it. Make the last date explicit when needed.
- Rolling windows defined as fixed day counts: Use DATESINPERIOD with MONTH granularity for R12, not LASTDATE-365 (which is day-based and can misalign months).
- Multiple active date relationships: If facts have multiple date columns, use USERELATIONSHIP inside CALCULATE for alternative dates.
Quick self-check checklist
- [ ] Date table marked, continuous, and properly related
- [ ] Base measure exists and is reused
- [ ] Time windows applied through CALCULATE on 'Date'[Date]
- [ ] Visuals show expected totals around year boundaries
- [ ] Rolling measures stabilize after enough history
Exercises (do these now)
These mirror the exercises section on this page. Build the measures and validate with a simple matrix by Year and Month.
Exercise 1: Year-over-Year Sales
- Create [Sales Amount] = SUM(Sales[Amount])
- Create [Sales Amount YoY] using SAMEPERIODLASTYEAR
- Validate: For March 2024, it should show March 2023 sales
Hint
Apply the time window to the Date table inside CALCULATE.
Show solution
Sales Amount = SUM(Sales[Amount])
Sales Amount YoY =
CALCULATE([Sales Amount], SAMEPERIODLASTYEAR('Date'[Date]))Exercise 2: Rolling 12 Months
- Create [Sales Amount R12] using DATESINPERIOD ending at MAX('Date'[Date])
- Validate: At Dec 2024, it sums Jan–Dec 2024; at Jan 2024, it sums Feb 2023–Jan 2024
Hint
Use DATESINPERIOD with -12 MONTH and an EndDate = MAX('Date'[Date]).
Show solution
Sales Amount R12 =
VAR EndDate = MAX('Date'[Date])
RETURN CALCULATE(
[Sales Amount],
DATESINPERIOD('Date'[Date], EndDate, -12, MONTH)
)- [ ] Measures compile without errors
- [ ] Values align across year boundaries
- [ ] Rolling values stabilize after enough months
Practical projects
- Sales Performance Page: Cards for Sales, YoY%, MTD, YTD; a line chart for R12; a toggle for Fiscal vs Calendar YTD
- Subscription Health: Active Customers MTD, Churn Rate YoY, Rolling 90-Day Revenue (use DATESINPERIOD with DAY)
- Inventory Snapshot: Semi-additive pattern for end-of-month stock with LASTDATE and time-intelligence comparisons
Mini challenge
Create [Gross Margin % YoY] where:
- Gross Margin % = DIVIDE([Gross Margin], [Sales Amount])
- Gross Margin % YoY compares current Gross Margin % to last year
Show outline
Gross Margin = [Sales Amount] - SUM(Sales[COGS])
Gross Margin % = DIVIDE([Gross Margin], [Sales Amount])
Gross Margin % YoY =
VAR Prev = CALCULATE([Gross Margin %], SAMEPERIODLASTYEAR('Date'[Date]))
RETURN PrevOptionally compute Delta pp = [Gross Margin %] - [Gross Margin % YoY].
Learning path
- Now: Time Intelligence Patterns Basics
- Next: Advanced Rolling Windows and Seasonality
- Then: Fiscal Calendars and Multiple Date Columns (USERELATIONSHIP)
- Finally: Performance tuning for time intelligence (variables, filter context control)
Who this is for
- BI Developers building executive dashboards and recurring KPI reports
- Analysts migrating from Excel time formulas to DAX measures
- Data modelers who need robust date logic
Next steps
- Complete the exercises above
- Take the Quick Test below to confirm understanding (available to everyone; only logged-in users get saved progress)
- Apply the patterns to your own model using your Date table