Use case
The 4-5-4 calendar makes sales comparable between years. It divides each retail year into quarters of three months, and each quarter into weeks in a 4 – 5 – 4 pattern, so a retail month is either four or five weeks long. Every month therefore begins on the same weekday and contains the same number of Saturdays and Sundays as its counterpart a year earlier, which is what makes like-for-like sales reporting possible. Because a retail month varies in length, it cannot be derived arithmetically from a fixed-length interval. It has to be read from a calendar table that states, for every date, which retail period that date belongs to.Data modeling
The implementation has two parts:- A calendar cube over the calendar table, where the
week,month,quarter, andyeargranularities are overridden with pre-calculated columns. - A join from each cube with facts to that calendar cube.
Calendar table
Consider the following calendar table. Every row is a date, and the remaining columns state the retail periods that the date belongs to. In production, generate it with a data transformation tool and materialize it as a table:
The retail year 2024 begins on 2024-02-04. The month beginning on that date is four weeks
long, so the next one begins on 2024-03-03; that one is five weeks long, so the third
begins on 2024-04-07. Those three months make up the first retail quarter, and the second
one begins on 2024-05-05. That 4 – 5 – 4 sequence is exactly what no
interval can
express, and it is why these dates are pre-calculated rather than computed at query time.
The last two columns hold the date one retail month and one retail year earlier. They are
what make time shifts follow the retail calendar as
well.
Calendar cube
Setcalendar to true on the cube over the calendar table, and
override the granularities of its primary_key dimension:
sql must be named after a default one; retail_month would not compile.
See naming a granularity defined with sql for the rule and
for when to use interval instead.
Override the granularities on the dimension you group by. A calendar cube can expose
more than one time dimension, and an override applies only to the dimension it is defined
on. A query that groups by a dimension without the override falls back to
DATE_TRUNC and
returns Gregorian months, with no error. The same is true per granularity: this cube still
answers day with DATE_TRUNC, because day is not overridden.Cubes with facts
Join each cube with facts to the calendar cube on its own time dimension:primary_key.
A pair of cubes can only be joined once, so translating a second time dimension, such as
completed_at, needs a second calendar cube. Define it with
extends to inherit the granularities and time shifts, and repeat
calendar on it:
orders as well, on the second time dimension:
Querying
Queryorders.count by retail_calendar.date with the month granularity. The result is
grouped by retail months, not Gregorian ones:
The month beginning on 2024-03-03 spans five weeks; the ones around it span four. Grouping
by
week, quarter, and year works the same way, and each returns the retail period
rather than the Gregorian one.
Comparing with a prior period
Because the calendar cube also overrides the time shifts, a period-over-period measure compares a retail month with the retail month before it. Define it on the cube with facts, next to the measure it shifts:date_prev_month column, so it lands on the equivalent day
of the previous retail month rather than a calendar month earlier.
Measuring a period to date
A rolling window of typeto_date also follows the calendar. It
belongs on the cube with facts as well:
Pre-aggregations
A pre-aggregation over an overridden granularity must declare that granularity. A rollup onmonth is built from the retail_month_begins column and serves
queries at month: