Skip to main content
On this page

Model Fields Custom calculations

Period comparisons

Compare a measure with the period before, the same period last year, or any shift you set.

Audience
Data teams
Prerequisites
  • A query grouped by a date

A period comparison works a measure out again over another calendar period, such as last month or the same month last year. The Compare with menu writes it. In a query, it is a custom calculation. When Kater can prove that the numbers stay the same, it can become a reusable measure.

How it works

A period comparison does not read another result row. For each row, it calculates the measure again with the date moved back. This month’s row reads Orders over last month’s dates.

  • It reads periods the result does not show. The comparison reads the source rows, so last month does not need a row in the result. A date filter in WHERE moves back with it.
  • A missing period never falls back to an earlier row. The comparison reads the calendar period before. When that period has no rows, the value is what the measure gives over no rows: null for a sum, such as Units, and 0 for a count, such as Orders. A row lag reads the row before, whatever month it is. See Missing periods.
  • It needs a date at a calendar grain. Group the query by a date at a grain, or select a finite date range. The grain sets the period: a date by month compares months. The examples group by KATER__MONTH(shop.order_date), the order date by month.
  • It needs one date axis. When the query groups by one date axis, the comparison uses it. With more than one, name the axis with by =>.
  • The menu can add the date. On a query with no date, Compare with offers the comparisons of a monthly axis. Writing one adds the order date by month to the query first.

Syntax

KATER__PREVIOUS_PERIOD(measure, by => date_axis, coverage => 'matching_elapsed')
KATER__PERIOD_SHIFT(measure, by => date_axis, unit => 'month', offset => -3)
KATER__PERCENT_CHANGE(EVAL(measure), baseline)
PartWhat it doesExample
measureA reusable measure, written without EVAL. To compare a number column, make a measure of it first.shop.count_distinct__order_id
by =>The date axis, at its grain. It is optional when the query groups by one date axis. KATER__PERIOD_SHIFT and a comparison inside a MEASURE always need it.by => KATER__MONTH(shop.order_date)
coverage =>Optional. 'matching_elapsed', the default, or 'full_period'. See Coverage.coverage => 'full_period'
unit =>KATER__PERIOD_SHIFT only. The unit to shift by.unit => 'month'
offset =>KATER__PERIOD_SHIFT only. A whole number, not 0. A negative number is earlier, a positive number is later.offset => -3
KATER__PERCENT_CHANGE(current, baseline)The change from the baseline, as a fraction.KATER__PERCENT_CHANGE(EVAL(m), KATER__PREVIOUS_PERIOD(m, by => …))

The comparisons

ComparisonFunctionCompares withDate grains
Previous PeriodKATER__PREVIOUS_PERIODThe period before, at the axis’s grainDay, week, month, quarter, year
Same Period Last YearKATER__SAME_PERIOD_LAST_YEARThe matching period a year earlierDay, month, quarter
Same Weekday Last WeekKATER__SAME_WEEKDAY_LAST_WEEKSeven days earlierDay
Advanced Calendar ShiftKATER__PERIOD_SHIFTA signed shift you set with unit and offsetDay, week, month, quarter, year

The menu names a comparison by the axis’s grain. On a month axis, it offers Previous Month, Same Month Last Year and Advanced Calendar Shift. On a day axis, it offers Previous Day, Same Date Last Year, Same Weekday Last Week and Advanced Calendar Shift. With more than one date axis, it says Previous Period.

An advanced shift moves by a unit the axis’s grain can hold:

Axis grainUnits it can shift by
DayYear, quarter, month, week, day
WeekWeek
MonthYear, quarter, month
QuarterYear, quarter
YearYear

Three outputs

Each comparison has three outputs. In the New calculation dialog, Show under Settings selects one.

OutputShow in the dialogKater SQLFormat it gets
Comparison valueThe number thenKATER__PREVIOUS_PERIOD(m, by => …)The measure’s format
DifferenceHow much it changedEVAL(m) - KATER__PREVIOUS_PERIOD(m, by => …)The measure’s format
Percent changeHow much it changed, in %KATER__PERCENT_CHANGE(EVAL(m), KATER__PREVIOUS_PERIOD(m, by => …))percent(1)
  • Percent change divides by the absolute baseline. It is (current - baseline) / ABS(baseline), so a rise from a negative baseline is positive.
  • A zero or missing baseline gives null. There is no percent change from nothing.
  • Percent change is a fraction. percent(1) shows 0.099 as 9.9%. Never multiply it by 100.
  • Each output is its own field. Declare each one with its own CUSTOM_CALCULATION and name. AS gives a label only, so an output with no declaration has no field name, and no other model can read it.

Coverage

Coverage says what to compare with when the current period is not complete.

CoverageIn the dialogCompares with
matching_elapsed, the defaultSame days so farThe same days of the earlier period
full_periodThe whole month, at a month grainThe whole earlier period

The dialog asks it as If the month is not over, with the axis’s grain in place of month. A week axis has matching_elapsed only. Coverage changes a number only when a date filter cuts the current period short. Grouped by month with no date filter, each month compares with the whole month before. The sample shop’s orders end on 2026-09-30, the last day of a month. So every month of the shop is complete, and both coverages give the same numbers.

September 1 to 15 against August

How do the first half of September’s orders compare with August?

workspaces/shop/query_contracts/september_vs_august.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'previous_month_matching__count_distinct__order_id',
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date), coverage => 'matching_elapsed')
) AS "Orders (Previous Month, Same Days)",
CUSTOM_CALCULATION(
'previous_month_full__count_distinct__order_id',
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date), coverage => 'full_period')
) AS "Orders (Previous Month, Whole Month)"
FROM workspace.shop AS shop
WHERE
shop.order_date BETWEEN '2026-09-01' AND '2026-09-15'
GROUP BY
KATER__MONTH(shop.order_date)
Order MonthOrdersOrders (Previous Month, Same Days)Orders (Previous Month, Whole Month)
2026-09-015968145

The filter keeps September 1 to 15, with 59 orders. Same days so far compares with August 1 to 15, which had 68 orders. The whole month compares with all of August, 145 orders. Half a month against a whole month makes September look much worse than it is.

Examples

Orders compared with the previous month

How did each month’s orders change from the month before?

  1. In a query grouped by the order date by month, right-click Orders.
  2. Select Compare with › Previous Month.
  3. Under Settings, set Show to The number then, and select Create calculation.
  4. Do the same again with How much it changed, and with How much it changed, in %.
workspaces/shop/query_contracts/orders_vs_previous_month.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'previous_month__count_distinct__order_id',
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
) AS "Orders (Previous Month)",
CUSTOM_CALCULATION(
'previous_month_change__count_distinct__order_id',
EVAL(shop.count_distinct__order_id) - KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
) AS "Orders (Change vs Previous Month)",
CUSTOM_CALCULATION(
'previous_month_percent_change__count_distinct__order_id',
KATER__PERCENT_CHANGE(
EVAL(shop.count_distinct__order_id),
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
)
) AS "Orders (% Change vs Previous Month)" /* @format percent(1) */
FROM workspace.shop AS shop
GROUP BY
KATER__MONTH(shop.order_date)
ORDER BY
1
Order MonthOrdersOrders (Previous Month)Orders (Change vs Previous Month)Orders (% Change vs Previous Month)
2025-01-011410141null
2025-02-01155141149.9%
2025-03-01133155-22-14.2%
2025-04-011521331914.3%
2025-05-01143152-9-5.9%

The first 5 of 21 rows. February had 155 orders against January’s 141, a change of 14, and 14 / 141 = 9.9%. The shop has no orders before January 2025. Orders is a count, and a count of no rows is 0, so January’s comparison value is 0. Its percent change is null, because the baseline is 0.

The same month last year

How did each month of 2026 do against the same month of 2025?

  1. Right-click Orders.
  2. Select Compare with › Same Month Last Year.
  3. Set Show to The number then, and select Create calculation.
  4. Do the same again with How much it changed, in %.
workspaces/shop/query_contracts/orders_vs_last_year.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'same_month_last_year__count_distinct__order_id',
KATER__SAME_PERIOD_LAST_YEAR(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
) AS "Orders (Same Month Last Year)",
CUSTOM_CALCULATION(
'same_month_last_year_percent_change__count_distinct__order_id',
KATER__PERCENT_CHANGE(
EVAL(shop.count_distinct__order_id),
KATER__SAME_PERIOD_LAST_YEAR(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
)
) AS "Orders (% Change vs Same Month Last Year)" /* @format percent(1) */
FROM workspace.shop AS shop
WHERE
shop.order_date BETWEEN '2026-01-01' AND '2026-09-30'
GROUP BY
KATER__MONTH(shop.order_date)
ORDER BY
1
Order MonthOrdersOrders (Same Month Last Year)Orders (% Change vs Same Month Last Year)
2026-01-011451412.8%
2026-02-01132155-14.8%
2026-03-0115213314.3%
2026-04-01134152-11.8%
2026-05-011551438.4%
2026-06-01136150-9.3%

The first 6 of 9 rows. The WHERE keeps only 2026, but the comparison still reads 2025: the date filter moves back a year with it. January 2026 had 145 orders against 141 in January 2025, so it rose 2.8%.

The same weekday last week

How did each day of the last full week of September 2026 compare with the same weekday a week before? This comparison needs a day axis, so the query groups by the order date itself.

  1. In a query grouped by Order Date, right-click Orders.
  2. Select Compare with › Same Weekday Last Week.
  3. Select Create calculation.
workspaces/shop/query_contracts/orders_vs_last_week.sql
SELECT
shop.order_date,
shop.day_name__order_date,
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'same_weekday_last_week__count_distinct__order_id',
KATER__SAME_WEEKDAY_LAST_WEEK(shop.count_distinct__order_id, by => shop.order_date)
) AS "Orders (Same Weekday Last Week)"
FROM workspace.shop AS shop
WHERE
shop.order_date BETWEEN '2026-09-21' AND '2026-09-27'
GROUP BY
shop.order_date,
shop.day_name__order_date
ORDER BY
1
Order DateOrder Date Day NameOrdersOrders (Same Weekday Last Week)
2026-09-21Mon87
2026-09-22Tue57
2026-09-23Wed45
2026-09-24Thu32
2026-09-25Fri46
2026-09-26Sat59
2026-09-27Sun66

Each day reads the day seven days earlier. Monday, September 21, had 8 orders, and Monday, September 14, had 7.

Three months back

How do each month’s orders compare with the month three months before? Advanced Calendar Shift moves by any number of units you set.

  1. Right-click Orders.
  2. Select Compare with › Advanced Calendar Shift.
  3. Under Settings, set how far back to go with How far back and Counted in.
  4. Select Create calculation.
workspaces/shop/query_contracts/orders_3_months_back.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'shift_3_months_back__count_distinct__order_id',
KATER__PERIOD_SHIFT(
shop.count_distinct__order_id,
by => KATER__MONTH(shop.order_date),
unit => 'month',
offset => -3
)
) AS "Orders (3 Months Back)"
FROM workspace.shop AS shop
WHERE
shop.order_date BETWEEN '2026-04-01' AND '2026-09-30'
GROUP BY
KATER__MONTH(shop.order_date)
ORDER BY
1
Order MonthOrdersOrders (3 Months Back)
2026-04-01134145
2026-05-01155132
2026-06-01136152
2026-07-01137134
2026-08-01145155
2026-09-01137136

offset => -3 is three months earlier. April 2026 reads January 2026, which had 145 orders. A positive offset compares with a later period.

Revenue compared with last month

Revenue cannot be added up across currencies. See Measures you cannot add up. So a comparison of Revenue must keep Currency in the grouping, as Revenue itself must.

workspaces/shop/query_contracts/revenue_vs_previous_month.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
shop.currency,
EVAL(shop.sum__amount),
CUSTOM_CALCULATION(
'previous_month__sum__amount',
KATER__PREVIOUS_PERIOD(shop.sum__amount, by => KATER__MONTH(shop.order_date))
) AS "Revenue (Previous Month)" /* @format currency(currency, 2) */
FROM workspace.shop AS shop
WHERE
shop.order_date BETWEEN '2026-08-01' AND '2026-09-30'
GROUP BY
KATER__MONTH(shop.order_date),
shop.currency
ORDER BY
1,
2
Order MonthCurrencyRevenueRevenue (Previous Month)
2026-08-01EUR€3,678.81€4,266.29
2026-08-01USD$20,484.84$19,266.53
2026-09-01EUR€5,353.49€3,678.81
2026-09-01USD$17,693.37$20,484.84

Each currency compares with its own last month. September’s euro row reads August’s euros, €3,678.81. Without Currency in the GROUP BY, the compiler refuses the query with not_additive_across: “Revenue cannot be added up across Currency. Group by Currency or filter it to one value.”

The previous month in Monthly Revenue

The sample shop’s monthly_revenue query declares last month’s Revenue beside each month’s. It groups by month and currency, and it writes the coverage out:

workspaces/shop/query_contracts/monthly_revenue.sql
CUSTOM_CALCULATION(
'previous_month_value__sum__amount',
KATER__PREVIOUS_PERIOD(
shop.sum__amount,
by => KATER__MONTH(shop.order_month),
coverage => 'matching_elapsed'
)
) AS "Revenue (Previous Month)" /* @format currency(currency, 2) */
Order MonthCurrencyRevenueRevenue (Previous Month)
2025-01-01EUR€4,961.67null
2025-02-01EUR€4,727.40€4,961.67
2026-09-01EUR€5,353.49€3,678.81
2025-01-01USD$16,445.68null
2025-02-01USD$17,070.42$16,445.68
2026-09-01USD$17,693.37$20,484.84

Six of its 42 rows, and four of its seven columns. Revenue is a sum, and a sum of no rows is null. So January 2025, with no month before it, shows null in both currencies. The same query also keeps a running total and a percent delta. Those are row windows, declared with CUSTOM_CALCULATION. See Running totals, ranks and windows.

Missing periods

A period comparison reads the calendar period before. A row lag reads the row before. They differ when a period has no rows. This query keeps web orders with no region, which skip some months:

workspaces/shop/query_contracts/web_units_missing_months.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.sum__quantity),
CUSTOM_CALCULATION(
'previous_month__sum__quantity',
KATER__PREVIOUS_PERIOD(shop.sum__quantity, by => KATER__MONTH(shop.order_date))
) AS "Units (Previous Month)",
CUSTOM_CALCULATION(
'lag__sum__quantity',
KATER__LAG(EVAL(shop.sum__quantity)) OVER (ORDER BY KATER__MONTH(shop.order_date) ASC)
) AS "Units (Lag)"
FROM workspace.shop AS shop
WHERE
shop.channel = 'web'
AND shop.region IS NULL
GROUP BY
KATER__MONTH(shop.order_date)
ORDER BY
1
Order MonthUnitsUnits (Previous Month)Units (Lag)
2025-01-019nullnull
2025-04-017null9
2025-06-012null7
2025-07-01422
2025-08-01344
2025-09-01233

The first 6 of 12 rows. March 2025 has no such orders, so April’s previous month is null. The lag reads the row before, January, and shows its 9 units as if they were March’s. When the months follow each other, as in July to September, the two agree. Units is a sum, so a month with no rows is null. For a count, such as Orders, it is 0.

Convert a comparison to a measure

A comparison in a query is a custom calculation, but it is not a window. When the comparison is complete and reads a reusable measure, its menu offers Edit › Convert to a measure. The conversion keeps the name, the label, the axis, the grain and the shift. It changes only the wrapper, from CUSTOM_CALCULATION to MEASURE. A reader then works the comparison out again at its own grouping, from the current and the earlier totals.

  • It is offered only after a proof. Kater offers it only when it can prove that the measure gives the same numbers. A WHERE, a join, a HAVING or a QUALIFY in the query blocks it.
  • All three outputs convert. A percent change works out both totals again before it divides. It never averages the percentages a result shows.
  • A row lag never converts. Lag, Delta and Percent Delta read the row before, not the period before, so no measure gives their numbers. Changing their wrapper does not make them calendar comparisons.

A previous month a reader can regroup

This query counts orders by month and channel, with last month’s orders as a measure:

workspaces/shop/query_contracts/orders_by_channel.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
shop.channel,
EVAL(shop.count_distinct__order_id),
MEASURE(
'previous_month__count_distinct__order_id',
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
) AS "Orders (Previous Month)"
FROM workspace.shop AS shop
GROUP BY
KATER__MONTH(shop.order_date),
shop.channel
ORDER BY
1,
2

A query that reads orders_by_channel groups by month only:

workspaces/shop/query_contracts/monthly_orders.sql
SELECT
q.order_date,
q.previous_month__count_distinct__order_id
FROM query.orders_by_channel AS q
GROUP BY
q.order_date
ORDER BY
1
Order MonthOrders (Previous Month)
2025-01-010
2025-02-01141
2025-03-01155
2025-04-01133

The first 4 of 21 rows. The reader works the measure out again for each month across all channels: February’s previous month is January’s 141 orders. As a custom calculation, it would be one finished value for each month and channel, not a total the reader can work out again. A reader must still group by the date axis, or the compiler refuses it.

Filter a comparison

Filter by this number on a comparison writes the condition in HAVING. The comparison is a value the query works out at its grouping, not a window. A comparison runs no window, so it also offers Is In The Top and Is In The Bottom, which go in QUALIFY.

Months that grew more than 15%

workspaces/shop/query_contracts/orders_growth_over_15_percent.sql
SELECT
KATER__MONTH(shop.order_date) AS "Order Month",
EVAL(shop.count_distinct__order_id),
CUSTOM_CALCULATION(
'previous_month_percent_change__count_distinct__order_id',
KATER__PERCENT_CHANGE(
EVAL(shop.count_distinct__order_id),
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
)
) AS "Orders (% Change vs Previous Month)" /* @format percent(1) */
FROM workspace.shop AS shop
GROUP BY
KATER__MONTH(shop.order_date)
HAVING
KATER__IS_GREATER_THAN(
KATER__PERCENT_CHANGE(
EVAL(shop.count_distinct__order_id),
KATER__PREVIOUS_PERIOD(shop.count_distinct__order_id, by => KATER__MONTH(shop.order_date))
),
0.15
)
ORDER BY
1
Order MonthOrdersOrders (% Change vs Previous Month)
2025-08-0115116.2%
2026-01-0114516.9%
2026-03-0115215.2%
2026-05-0115515.7%

The filter keeps four of the 21 months. Each one still compares with its month before, though that month is not in the result: August 2025’s 151 orders against July’s 130.

Common errors

CodeCauseFix
ambiguous_comparison_axisThe query groups by more than one date axis, or by none, and the comparison names no by =>.Apply the fix that names the axis, such as Compare using Order Month (month), or add by =>.
invalid_comparison_unitKATER__PERIOD_SHIFT shifts by a unit the axis’s grain cannot hold, such as days on a month axis. The compiler says “Month comparisons support shifts by year, quarter, month.”Apply a fix such as Shift by month.
invalid_comparison_offsetoffset is 0, or not a whole number.Apply Compare one unit earlier (-1) or Compare one unit later (1).
invalid_comparison_coveragefull_period on a week axis.Apply Use matching elapsed dates.
invalid_business_functionThe comparison cannot be worked out as written: the measure is inside EVAL, the query neither groups by the date axis nor selects a finite date range, or the comparison does not exist at the axis’s grain, such as Same Period Last Year on a week axis.Write the measure without EVAL, group by the date axis, or select a comparison the grain offers.
retired_comparison_functionAn old function, such as KATER__PERIOD_DELTA or KATER__PREVIOUS_PERIOD_VALUE.Use KATER__PREVIOUS_PERIOD or KATER__PERIOD_SHIFT, with EVAL(m) - for a difference and KATER__PERCENT_CHANGE for a percent change.
not_additive_acrossA comparison of a measure such as Revenue, with Currency not in the grouping.Group by Currency, or filter it to one value.