Window Functions¶
A window function produces one result for each input row, using that row and other related rows. Unlike a grouped aggregate, it does not collapse those rows into a single output row. For example, a windowed SUM can attach a running total to every row.
Partitions, Peer Groups, and Frames¶
Three concepts determine which rows a window function can use:
- Partition: rows with the same values for all partitioning expressions. Each partition is processed independently; a frame never crosses a partition boundary. Null partitioning values are considered equal. Without partitioning expressions, the entire input is one partition.
- Peer group: rows within a partition that share the same values for all ordering expressions. Two null ordering values are considered equal. Ordering arranges rows within a partition, but does not break ties between peers. Without ordering expressions, all rows in a partition are peers.
- Frame: the subset of a partition selected by lower and upper bounds relative to the current row, which is the row whose result is being calculated. The frame can change as the current row changes. Whether a bound counts rows or measures a distance in ordering values depends on the bound mode.
For example, partition by team and order by score ascending:
| Row | team | score | amount | Peer group within the partition |
|---|---|---|---|---|
| A | North | 10 | 2 | 1 |
| B | North | 20 | 3 | 2 |
| C (current) | North | 20 | 5 | 2 |
| D | North | 30 | 7 | 3 |
| E | North | 50 | 11 | 4 |
| F | South | 20 | 100 | 1 |
Rows B and C are peers. Row F has the same score, but belongs to a different partition. For current row C, these frames give different results for SUM(amount):
| Mode | Lower bound | Upper bound | Rows in frame | Result |
|---|---|---|---|---|
| ROWS | CurrentRow | CurrentRow | C | 5 |
| RANGE | CurrentRow | CurrentRow | B, C | 8 |
| ROWS | Preceding(1) | CurrentRow | B, C | 8 |
| RANGE | Preceding(10) | CurrentRow | A, B, C (scores 10 through 20) | 10 |
| ROWS | CurrentRow | Following(1) | C, D | 12 |
| RANGE | CurrentRow | Following(10) | B, C, D (scores 20 through 30) | 15 |
Preceding(n) and Following(n) are shorthand here for bounds with an offset_expr evaluating to n. The ROWS examples assume B appears before C; ordering only by score does not guarantee that order among peers. Adding a tie-breaking ordering expression makes the row order deterministic, but also changes which rows are peers.
The RANGE Following(10) example includes B even though it is displayed before C: the CurrentRow lower bound starts at the beginning of their shared peer group.
RANGE frames use peer groups, but peer groups are not a bound mode of their own: Substrait currently defines ROWS and RANGE, not a mode that counts peer groups.
Window Bindings¶
A window binding inherits the aggregate binding properties and adds the following:
| Property | Protobuf field | Meaning and default |
|---|---|---|
| Partitioning | partitions | Partitioning expressions; none means one partition for the entire input. |
| Ordering | sorts | Ordering expressions within each partition, highest priority first. Optional, except as required by the bound mode rules. This also supplies the inherited aggregate ordering: rows in the frame are passed to the function in this order. |
| Bound mode | bounds_type | Required: BOUNDS_TYPE_ROWS or BOUNDS_TYPE_RANGE. Consumers must reject BOUNDS_TYPE_UNSPECIFIED. |
| Lower bound | lower_bound | Inclusive start of the frame; defaults to Unbounded if not set. |
| Upper bound | upper_bound | Inclusive end of the frame; defaults to Unbounded if not set. |
In a consistent partition window operation, partition_expressions and sorts are shared by all functions. Each function still has its own bound mode and frame bounds. The same rules below apply to both representations.
Frame Bounds¶
| Bound | Meaning |
|---|---|
Unbounded | Start of the partition for a lower bound; end of the partition for an upper bound. |
CurrentRow | The current row in ROWS mode; the start or end of its peer group in RANGE mode. |
Preceding | An offset toward rows or values earlier in the declared ordering. |
Following | An offset toward rows or values later in the declared ordering. |
Both bounds are inclusive, and only rows in the current partition can belong to the frame.
A frame need not contain the current row. If the lower bound is after the upper bound, the frame is empty. The function’s empty-frame semantics apply, such as SUM yielding null and COUNT(*) yielding zero.
Bound Modes¶
ROWS¶
BOUNDS_TYPE_ROWS counts rows in the declared ordering:
CurrentRowmeans only the current row, even when it has peers.PrecedingandFollowingoffset expressions must have typeint64and evaluate to a non-negative valuen. This specifies an inclusive frame boundarynrows before the current row forPreceding, or after it forFollowing.- Without ordering, no row order is guaranteed. With ordering, the relative order of peers is still not guaranteed.
In the example above, one preceding row means B and one following row means D when C is current and the displayed order is used. These offsets count individual rows, not groups of rows sharing a score.
RANGE¶
BOUNDS_TYPE_RANGE measures distances in ordering values, rather than counting rows:
CurrentRowmeans the first row of the current peer group for a lower bound and the last row of that group for an upper bound.- If either bound is
PrecedingorFollowing, there must be exactly one ordering expression, and it must not useSORT_DIRECTION_CLUSTEREDor a custom comparison function. - An offset must be a non-negative distance with a compatible type.
- When the current row’s ordering value is null, an offset bound is equivalent to
CurrentRow.
The single-ordering-expression restriction does not apply when both bounds are CurrentRow or Unbounded (or omitted). With multiple ordering expressions, peer equality uses all of them.
For an offset d and current ordering value v, compute the boundary in the direction of the declared ordering:
| Sort direction | Preceding(d) | Following(d) |
|---|---|---|
| Ascending | subtract(v, d) (lower values) | add(v, d) (higher values) |
| Descending | add(v, d) (higher values) | subtract(v, d) (lower values) |
A boundary value need not occur in the input. For example, with ascending scores, current score 20, and bounds Preceding(7) through CurrentRow, the frame includes scores from 13 through 20, including every peer at score 20.
Offset Type Compatibility¶
Let T be the ordering expression’s type and D the offset expression’s type. D must be compatible with T: add(T, D) -> T and subtract(T, D) -> T must be defined.
For example, the standard extensions allow:
Ordering type T | Offset type D | Boundary type |
|---|---|---|
i64 | i64 | i64 |
precision_timestamp<P> | interval_day<P> | precision_timestamp<P> |
Offset Expressions¶
The following rules apply to offset_expr in both Preceding and Following, in either bound mode:
- Field references resolve against the input relation schema. The expression must behave as if evaluated once per input row, and must not contain window or aggregate functions.
- A null or negative result is invalid. Use the opposite bound direction, not a negative offset, to move the other way.
- A statically-known zero offset must be represented as
CurrentRow. If a non-literal expression evaluates to zero for a row, the bound is equivalent toCurrentRowfor that row.
The strictly positive integer offset field is deprecated in favor of offset_expr and will be removed in a future release. Producers must set at least one. Because offset has no field presence, an offset of 0 counts as unset, and consumers must reject a Preceding or Following bound with no offset_expr and an offset of 0.
When offset_expr is present, consumers must use it and ignore offset. Producers must write offset_expr and must also write offset whenever an exact legacy representation exists; otherwise, they must write only offset_expr. An exact legacy representation requires one fixed, strictly positive int64 offset that is semantically equivalent for every input row; literal syntax is not required.
Function Signatures¶
Window function signatures inherit all properties of aggregate functions and add:
| Property | Description | Default |
|---|---|---|
| Window Type | STREAMING or PARTITION: whether the function can produce results without seeing the entire partition. SUM can operate in a streaming manner; NTILE needs the entire partition. | PARTITION |
Window Type describes the function’s data needs, not its bound mode. Aggregate functions such as AVG, COUNT, MAX, MIN, and SUM can be used as window functions with Window Type STREAMING.