3.2 · Time intelligence: running totals (YTD) & previous-year comparison

Level 3 · Advanced analysis

3.2Time intelligence: running totals (YTD) & previous-year comparison

Goal: Build the steering quartet (Sales YTD, Sales LY, change in value and in %) and place it in a time matrix that answers management at a glance.
Estimated time: 22 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • The No. 1 question of any management — "where do we stand vs last year?" — is answered with three functions: TOTALYTD (running total), SAMEPERIODLASTYEAR (same period last year) and DATEADD (generic offset).
  • Absolute prerequisite: the marked date table — without it, these functions return wrong results, especially the running total and the comparison.
  • The change is always expressed in value AND in %: the two measures go as a pair, and the whole forms the steering quartet.

Running mini-case — Zineb answers management at a glance

The management of Zineb's agri-food SME asks every Monday: "where do we stand compared with last year?". Rather than recalculating by hand, Zineb applies the section: she first checks that her date table is properly marked (absolute prerequisite), then builds the quartet — Sales YTD = TOTALYTD([Total Sales]; Dates[Date]) for the running total since January, Sales LY = CALCULATE([Total Sales]; SAMEPERIODLASTYEAR(Dates[Date])) for the same period a year earlier, then the change in value and in %. She places them in a Month × 4 measures matrix: the answer to management fits on a single visual, and the running total climbs before starting again in January.

Standalone case for this section: it returns in the course, the diagram, the script and the 48h challenge of section 3.2.

1. The three flagship functions

Time intelligence in DAX answers the No. 1 question of any management with three functions: TOTALYTD (the running total since the start of the year), SAMEPERIODLASTYEAR (the same period a year earlier) and DATEADD (the generic, more flexible offset). They never calculate "on their own": they move a period in time.

2. The non-negotiable prerequisite

Absolute prerequisite: the date table marked as a date table. Without it, these functions lie — an empty Sales LY or a wrong running total almost always comes from there. Zineb checks this setting before doubting her formula.

3. The steering quartet

We assemble the standard quartet of any management report: Sales YTD, Sales LY, change in value and change in %. These measures are built once, properly, and copied from model to model. The change is always expressed in value AND in %: the two go as a pair.

Common misinterpretation: "Time intelligence is magic: I write TOTALYTD and it works without preparing anything; the change can stay in value only, and last year's figure is right even without history." No, triple mistake. Without a marked date table, TOTALYTD and SAMEPERIODLASTYEAR lie — it is the absolute prerequisite. The change is expressed in value AND in %: value alone does not tell the relative magnitude. And an empty Sales LY is often normal when the history does not cover the previous year — we check the table, the relationship, then the history, in that order.
The steering quartet Cumulate, compare, quantify the gap: the quartet that answers "where do we stand?". Sales YTD TOTALYTD([TotalSales]; Dates[Date])Sales LY SAMEPERIODLASTYEARChange Value then %(DIVIDE)
Cumulate, compare, quantify the gap: the quartet that answers "where do we stand?".
Text version of the diagram (SEO / screen readers / translation): 1) Zineb, a management controller in Algiers, answers management's question: "where do we stand vs last year?". 2) She checks the prerequisite — the marked date table — then builds Sales YTD = TOTALYTD([Total Sales]; Dates[Date]) for the annual running total and Sales LY = CALCULATE([Total Sales]; SAMEPERIODLASTYEAR(Dates[Date])) for the same period a year earlier. 3) She completes the change in value ([Total Sales] - [Sales LY]) and in % (DIVIDE of the gap by [Sales LY]), then places everything in a Month × 4 measures matrix. Lesson: time intelligence always rests on the marked date table, and the change goes as a pair, value + percentage.

🔎 Concrete example — 🔎 Concrete example — The time measures

The standard quartet, reusable on all of Zineb's models:

Revenue YTD = TOTALYTD([Total Revenue], Dates[Date])
Revenue PY  = CALCULATE([Total Revenue], SAMEPERIODLASTYEAR(Dates[Date]))
Growth      = [Total Revenue] - [Revenue PY]
Growth %    = DIVIDE([Total Revenue] - [Revenue PY], [Revenue PY])   // % format

Reading: TOTALYTD cumulates since January and starts again at zero at the beginning of the year; SAMEPERIODLASTYEAR shifts the calculation one year back.

Lesson: DIVIDE([Total Sales] - [Sales LY]; [Sales LY]) gives the relative gap — the change goes as a pair, value + %.

Section vocabulary

Time intelligence
The family of DAX functions that manipulate periods (running totals, offsets, comparisons), all based on the date table.

Simple example: Zineb uses it for the SME's annual running total and previous-year comparison.

What it is not: a simple date filter placed by hand on a visual.

Reflex: compare periods → time intelligence.

TOTALYTD
The annual running total since the start of the year: TOTALYTD([Total Sales]; Dates[Date]).

Simple example: Zineb tracks the cumulative sales since January for management.

What it is not: the month's sales alone, with no running total.

Reflex: annual running total → TOTALYTD.

SAMEPERIODLASTYEAR
The same period a year earlier: CALCULATE([Total Sales]; SAMEPERIODLASTYEAR(Dates[Date])).

Simple example: Zineb compares the current month with the same month last year.

What it is not: any period with no date reference.

Reflex: previous-year comparison → SAMEPERIODLASTYEAR.

DATEADD
The generic offset: DATEADD(Dates[Date]; -1; MONTH) for the previous month — more flexible than the dedicated functions.

Simple example: Zineb uses it for a tailor-made offset.

What it is not: a function frozen on a single type of period.

Reflex: tailor-made offset → DATEADD.

Previous-year change
The final duo: Change = [Sales] - [Sales LY] and Change % = DIVIDE([Sales] - [Sales LY]; [Sales LY]).

Simple example: Zineb displays the gap in value and in % side by side.

What it is not: a change in value alone, with no relative magnitude.

Reflex: always value AND percentage.

In practice — In practice — The steering quartet

  1. Create Sales YTD = TOTALYTD([Total Sales]; Dates[Date]) and check in a Month × measures matrix: the running total climbs then starts again in January, as with Zineb.
  2. Create Sales LY = CALCULATE([Total Sales]; SAMEPERIODLASTYEAR(Dates[Date])) (requires history over two years — duplicate the sales shifted by one year in the CSV if needed).
  3. Complete Change % = DIVIDE([Total Sales] - [Sales LY]; [Sales LY]) in % format.
  4. Assemble the steering matrix: Month in rows, the 4 measures as values — the answer to "where do we stand?" fits on one visual.
The standard time measures of any management dashboard — reusable on all your future models.

Step-by-step script — Zineb's quartet (10 min)

1) Check that your date table is marked as a date table (Table tools). 2) Create Sales YTD = TOTALYTD([Total Sales], Dates[Date]). 3) Create Sales LY = CALCULATE([Total Sales], SAMEPERIODLASTYEAR(Dates[Date])). 4) Add Change = [Total Sales] - [Sales LY] and Change % = DIVIDE([Total Sales] - [Sales LY], [Sales LY]) in %. 5) Place Month in rows and the 4 measures as values: read the answer to management at a single glance.

An empty Sales LY? Check in order: date table marked, active relationship on the right column, history covering the previous year.

Copy-paste template — the time quartet

Revenue YTD = TOTALYTD([Total Revenue], Dates[Date])
Revenue PY  = CALCULATE([Total Revenue], SAMEPERIODLASTYEAR(Dates[Date]))
Growth      = [Total Revenue] - [Revenue PY]
Growth %    = DIVIDE([Total Revenue] - [Revenue PY], [Revenue PY])   // %

Prerequisite: date table MARKED. Reflex: growth = value + %.

Annual running total

Zineb tracks the cumulative sales since January (TOTALYTD): the curve climbs then starts again each new year.

Same month last year

Zineb compares the current month with the same month a year earlier (SAMEPERIODLASTYEAR) to smooth seasonality.

Previous month

Zineb shifts by one month with DATEADD to follow the short-term trend.

Shifted fiscal year

Closing in June: Zineb passes the fiscal year-end date to TOTALYTD to respect the SME's fiscal year.

✓ Best practices

  • Rely on the marked date table: it is the non-negotiable prerequisite.
  • Build these measures once, properly: they are copied from model to model.
  • Express the change in value AND in % (the two go as a pair).

✗ Common pitfalls

  • Forgetting to mark the date table (the functions lie).
  • Routing the date relationship through the wrong column.
  • Expecting a Sales LY with no history covering the previous year.

Key takeaways

  • All time intelligence rests on the MARKED date table: the prerequisite was not optional.
  • TOTALYTD, SAMEPERIODLASTYEAR, DATEADD: three functions cover the usual requests.
  • The change is expressed in value AND in %: the two measures go as a pair.
  • Build these measures once, properly: they are copied from model to model throughout your career.

48h challenge — apply it tomorrow

Build Zineb's steering quartet on your data: Sales YTD, Sales LY, change in value and in %. Place it in a Month × measures matrix, then verify that the running total does start again in January and that your date table is marked.

Section exit checklist
  • I can write an annual running total with TOTALYTD.
  • I can compare a period with the previous year using SAMEPERIODLASTYEAR.
  • I know the prerequisite: the marked date table.
Self-assessment 1-5: can I explain why the date table must be marked?Self-assessment 1-5: did I assemble the quartet Sales YTD / LY / change?

Frequently asked questions

My Sales LY shows empty or wrong: why?

Three suspects, to check in this order (one of them is the cause 95% of the time): the date table is not marked as a date table; the relationship goes through the wrong date column (or is inactive); the history does not cover the previous year (January's LY is then empty, which is normal). Always start with the marked date table.

How do I handle a shifted fiscal year (closing in June)?

TOTALYTD accepts a third argument, the fiscal year-end date: TOTALYTD([Sales]; Dates[Date]; "30/06") cumulates since 1 July. For more complex needs, we enrich the date table with fiscal columns (FiscalYear, FiscalMonth) — the beauty of having your own controlled calendar table.

Check your understanding

What is the absolute prerequisite for time intelligence to work?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned