modellerUpdated 2026-08-14

Understanding Window Functions

What this covers

Tessallite's window variants — trailing_n, moving_avg_n, and lag — compute values over ordered sequences. Each variant is a window function applied at query time. When a semantic time grain is resolved, the source and aggregate routes use the same period key and reject sparse frames rather than silently widening them. This article explains what the window sees, what happens when data has gaps, and how partial windows behave.

Which variants use windows

VariantWindow typeWindow size
trailing_nSum over preceding rowsN rows (default 12)
moving_avg_nAverage over preceding rowsN rows (default 30)
lagSingle row N places earlierN (default 1)

These variants do not require a calendar type or a time hierarchy. They only need a date dimension in the query grain to order rows.

How windows are measured

Window variants use a ROWS frame, but at a resolved time grain the period key must also be contiguous. For a query grouped by month:

The physical frame is ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW. A period-key span/count guard makes that row frame period-safe after grouping and filtering; it does not insert zero-valued rows for empty periods.

The period key follows the selected calendar rather than assuming Gregorian months. Hourly rows use a contiguous hour index. Standard, Hijri, and retail 4-4-5 calendars use the actual period number on a 12-period year scale; a retail 53rd week remains part of period 12, so period 12 to period 1 is adjacent. Fiscal calendars derive period 1 from the configured fiscal start month, so March to April is adjacent for an April-start year.

Partial windows at series boundaries

A window variant needs the full N rows to compute its advertised value. At the start of a series, fewer than N rows exist. This is a partial window.

What happens

Tessallite does not pad missing rows with zeros. The window function only sees the rows that are actually present relative to the current row. The result is a best-effort computation over however many rows are available:

The label "3-month moving average" describes the window size, not a guarantee that every data point at the start of the series is backed by three full months. Tools that chart these values should consider annotating or de-emphasising the leading partial-window points.

Lag at boundaries

lag(N) at the boundary returns NULL because there is no row N positions earlier. Tessallite does not substitute zero — zero is a real value and would silently distort the comparison. A missing lag means the comparison cannot be made, and the absence is explicit.

Gaps in data

If the source data has no rows for a period inside a requested frame, the window does not reach farther back to compensate. The affected trailing_n, moving_avg_n, or lag value is NULL; it does not insert a zero for the missing period.

Sparse data

If most periods are missing, window variants produce explicit NULL values until the retained frame is contiguous:

The window size remains a ceiling at the start of a series: an initial frame may contain fewer than N contiguous rows. A gap inside that frame is different and is returned as NULL.

Empty periods are skipped, not counted as zero

Tessallite does not count an empty period as zero. A moving_avg_3 on a series where January has data, February is missing, and March has data returns NULL for March rather than producing (Jan + 0 + Mar)/3 or reaching back to an earlier row. This makes the missing-period condition explicit without inventing a value.

The trade-off is that an initial "N-period window" can contain fewer than N periods. If you need zero-filled calendar windows, use a calendar table or a hand-rolled expression with date filters; the window guard intentionally returns NULL rather than fabricating zeros.

Example — sparse trailing sum

Consider a query grouped by month with trailing_3 on a measure whose source data has gaps:

MonthRevenuetrailing_3
Jan100100
Feb120220
Mar(no data)(no row)
Apr90(NULL — March gap is inside the frame)
May110(NULL — March gap is inside the frame)
Jun130330
Jul80420

March has no row at all — the source data for March is missing. April's trailing_3 is NULL because the frame's period-key span includes a missing month. The window does not silently substitute an older existing row.

June is the first shown row whose retained frame is contiguous after the March gap leaves the frame; its partial result is Apr + May + Jun = 330. July continues that contiguous sequence with May + Jun + Jul = 420.

The period guard keeps the physical positional frame honest: rows are still not padded, and a missing period is visible as NULL. If you need a zero-filled calendar window, model the calendar rows explicitly.

Period-to-date vs. window — the distinction

BehaviourPeriod-to-date (ytd, qtd, mtd, wtd)Window (trailing_n, moving_avg_n, lag)
Calendar awarenessYes — resets at period boundariesPeriod-key adjacency is enforced when a time grain is resolved
Requires hierarchy + calendar typeYesNo
Requires calendar tableExpression-based for standard/fiscal/ISO/Thai; physical table for Hijri/retail 4-4-5No
Partial periodsWell-defined — YTD on Jan 10 is Jan 1-10Same partial-window rules apply
GapsEach period window is computed independently; gaps are gapsNULL while a gap is inside the requested frame; initial partial frames are allowed
Materialised in aggregatesSometimes — a period-variant query can be served from an aggregate when the query shape is proven safe; otherwise it is computed from the base aggregate or the sourceYes — window-based variants materialise in CTAS

When not to use window variants

Related