In DAX, why does SAMEPERIODLASTYEAR return blank or wrong values without a proper date table?
answer
- these functions return a table of dates
- where do those dates come from
- what happens on days with no sales
- one model setting changes filter removal
- sometimes blank is the honest answer
basics
~20 sDAX time-intelligence functions shift a set of dates and reapply it as a filter, which requires a dedicated date table with one row per contiguous calendar day, related to the fact table and marked as a date table. Gaps, duplicates, or filtering the fact table's own date column break the shift.
solid answer
~50 s`SAMEPERIODLASTYEAR ( 'Date'[Date] )` takes the dates in the current filter context, shifts them back one year, and returns that set of dates as a table — which you then feed to CALCULATE as a filter. For that to work, the argument must be a column of a real date table: one row per day, contiguous, covering whole calendar years across the whole range the model reports, no duplicates, related to the fact table, and set as the model's date table. Pass the fact table's own date column and the set has gaps wherever nothing was sold, so the shifted period silently loses days. Mark as Date Table matters too: it makes the engine clear other filters on that table when applying the shifted date filter, which is what keeps a year or month column from fighting the shift. A blank result can also just mean there was genuinely no data a year ago — check that before assuming a defect.
code
dax · 12 lines-- prior-year comparison over the date dimension
Sales LY = CALCULATE ( [Total Sales], SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
-- year to date, fiscal year ending 30 June
Sales FYTD = TOTALYTD ( [Total Sales], 'Date'[Date], "6/30" )
-- rolling 12 months ending at the last visible date
Sales R12M =
CALCULATE (
[Total Sales],
DATESINPERIOD ( 'Date'[Date], MAX ( 'Date'[Date] ), -12, MONTH )
)go deeper
Recall that prior-year and year-to-date measures need a separate date table related to the facts, and that these functions are used inside CALCULATE rather than on their own.
Explain that these functions return a shifted set of dates used as a filter, and list the date table's requirements: contiguous days, complete years, no duplicates, related and marked.
Debug a real blank or wrong comparison methodically — inspect the returned date set, verify contiguity and the marked table, check which relationship is active — and distinguish a genuine no-data-last-year blank from a model defect.
Own the calendar as a shared asset: fiscal year ends, retail 4-4-5 periods and holiday flags belong in one governed date dimension serving every model, not re-derived per report with Auto date/time.
## What a time-intelligence function actually returns Most DAX time-intelligence functions are table functions. `SAMEPERIODLASTYEAR ( 'Date'[Date] )` does not compute a number; it takes the set of dates currently visible, shifts each one back a year, and returns the resulting set. `DATEADD`, `DATESYTD`, `DATESINPERIOD`, `PARALLELPERIOD` and `PREVIOUSMONTH` all work the same way. You use them as a CALCULATE filter argument: ``` Sales LY = CALCULATE ( [Total Sales], SAMEPERIODLASTYEAR ( 'Date'[Date] ) ) ``` And because a table-valued filter on a date column overwrites the existing filter on that column, the measure evaluates over last year's dates instead of this year's. Understanding that these functions produce *date sets*, not numbers, explains every failure mode below. ## Requirement one: a real date dimension The column you pass must come from a table that behaves as a calendar: exactly one row per day, no gaps, no duplicate dates, covering complete calendar years from the first January before your earliest fact to the last December after your latest. The completeness matters because year-to-date and prior-year logic reason about whole years; a calendar that starts in March truncates the first year silently. The classic mistake is passing `Sales[OrderDate]` — the fact table's own date column. It contains only days on which something happened and repeats a date once per transaction. Shifting that set back a year gives you last year's *transaction days*, not last year's *period*, so a month with a few quiet days quietly reports a smaller comparison base. Some engines also reject a date column containing duplicates outright for certain functions, which at least fails loudly. ## Requirement two: the relationship The date table must be related to the fact table, normally one-to-many from `'Date'[Date]` to the fact's date key, with the filter flowing from date to fact. Time intelligence changes the filter on the date table; if nothing propagates that to the facts, the measure returns the same number as the unshifted one. When a fact table has several dates — order, ship, due — only one relationship can be active, and the others need `USERELATIONSHIP` inside CALCULATE to be used for a given measure. ## Requirement three: Mark as Date Table Marking a table as the model's date table does something specific: when a time-intelligence filter is applied to the marked date column, the engine removes the other filters on that table. Without it, a `'Date'[Year]` filter coming from a slicer or a matrix header can remain in force and intersect with the shifted dates, producing a blank — you asked for 2023 dates while a year filter still insists on 2024. Marking the table is also what makes it safe to turn off Power BI's Auto date/time, which otherwise generates a hidden per-column calendar that bloats the model and cannot express a fiscal year. ## Fiscal calendars and 4-4-5 Standard time intelligence assumes the Gregorian calendar. A fiscal year ending in June is partly handled — `DATESYTD` and `TOTALYTD` accept a year-end date argument such as `"6/30"` — but 4-4-5 retail calendars, 13-period calendars and ISO weeks are not expressible with these functions at all. The durable answer there is to put explicit period keys in the date table (period index, week index) and write the offset logic yourself with `CALCULATE` and `FILTER` over those keys. Interviewers like this because it separates people who have only used the wizard from people who have built a retail model. ## When blank is the correct answer Before debugging, check the data. A product launched this year has no sales a year ago, so `Sales LY` is genuinely blank and the growth percentage is genuinely undefined. Hiding that behind a zero makes a false comparison; usually the right move is `DIVIDE` returning blank and a visual that suppresses the row. Similarly, the earliest year in the model has no prior year at all, which is a data-range fact rather than a formula error. ## A debugging order that works Drop the shifted dates into a table visual to see what set is actually being returned; check that the date table is contiguous and complete; confirm it is marked as a date table; confirm the relationship is active and pointing the right way; confirm the measure filters the *date table's* column and not the fact's; and only then look at the measure logic. Nine times in ten the answer surfaces in the first three steps.
- What does marking a table as a date table actually change for time intelligence?It tells the engine that this table is the model's calendar, so when a time-intelligence function applies a filter to the marked date column, the other filters on that table are removed. Without it, a Year or Month column filtered by a slicer or visual header can intersect with the shifted date set and blank the result. It also lets you disable Auto date/time safely.
- How do you compute prior-year sales by ship date when the active relationship uses order date?Wrap the calculation in CALCULATE with USERELATIONSHIP to activate the ship-date relationship for that evaluation, alongside the time-intelligence filter — for example CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]), USERELATIONSHIP('Date'[Date], Sales[ShipDate])). The activation lasts only for that CALCULATE, so order-date measures elsewhere are unaffected.
- Why do standard time-intelligence functions fail on a 4-4-5 retail calendar?They assume Gregorian months, quarters and years. A 4-4-5 calendar's periods do not align with month boundaries and its years contain 52 or 53 weeks, so shifting by a Gregorian year lands on the wrong week. The working approach is to store period and week index columns in the date table and write offsets against those keys with CALCULATE and FILTER instead.
saying these in an interview costs you the question
- Passing the fact table's date column to a time-intelligence function
- Assuming a date table can start at the first transaction
- Treating a blank prior-year value as always a formula bug
- Not knowing Mark as Date Table changes filter removal
- Expecting standard functions to handle a 4-4-5 or ISO-week calendar