Window Functions
Window functions compute a value for each row based on a set of rows related to that row — its "window". Unlike aggregate functions, window functions do not collapse rows; each input row retains its identity in the result.
Opteryx supports three families of window function, and they follow different rules. Read the family you need rather than generalising from the others.
| Family | Functions | ORDER BY inside OVER |
|---|---|---|
| Ranking | ROW_NUMBER, RANK, DENSE_RANK |
Required |
| Navigation | LAG, LEAD |
Required |
| Aggregate | every aggregate in the aggregate catalog | Not supported |
No family supports a frame specification (ROWS BETWEEN, RANGE BETWEEN).
Both families share a set of rules on where a window expression may appear. A window may sit inside a larger expression — mass / SUM(mass) OVER () computes percent-of-total directly — but it may not appear inside an aggregate's argument or inside another window, and SELECT * cannot be combined with one. Read Restrictions on Both Families for the shapes that are refused and the remedies each refusal names.
Syntax
SELECT <ranking_function>() OVER ( [ PARTITION BY <column> ] ORDER BY <column> [ ASC | DESC ] )
FROM <relation_name>;
SELECT <navigation_function>(<column> [, <offset>]) OVER ( [ PARTITION BY <column> ] ORDER BY <column> [ ASC | DESC ] )
FROM <relation_name>;
SELECT <aggregate_function>(<column>) OVER ( [ PARTITION BY <column> ] )
FROM <relation_name>;Ranking Functions
SELECT <ranking_function>() OVER ( [ PARTITION BY <column> ] ORDER BY <column> [ ASC | DESC ] )
FROM <relation_name>;Parameters
<ranking_function>:ROW_NUMBER()— 1..n within each partition, ties broken arbitrarily.RANK()— ties share a rank, and the next rank skips (1, 1, 3).DENSE_RANK()— ties share a rank, and the next rank does not skip (1, 1, 2).
PARTITION BY <column>— optional. Without it, the whole relation is one window; with it, numbering restarts in each partition.ORDER BY <column>— required for ranking functions; without it the numbering would have no defined meaning, so Opteryx rejects the query rather than returning an arbitrary answer.
Examples
ORDER BY Is Required
SELECT ROW_NUMBER() OVER (PARTITION BY id) FROM $planets;
-- ROW_NUMBER() requires an ORDER BY in its OVER (...) clause.
-- Add one, for example `OVER (ORDER BY column)`.Empty parentheses are rejected for the same reason — ROW_NUMBER() OVER () has no ordering, so there is nothing to number by. (OVER () is meaningful for aggregate windows, where there is nothing to order.)
A ranking function without an OVER clause at all is rejected the same way — these functions are window-only.
PARTITION BY Is Optional
SELECT name, RANK() OVER (ORDER BY id) AS rk
FROM $planets;Numbering Restarts per Partition
SELECT planetId, ROW_NUMBER() OVER (PARTITION BY planetId ORDER BY id) AS rn
FROM testdata.satellites;Multiple Ranking Functions from One Sort
Several ranking functions sharing the same PARTITION BY and ORDER BY are computed from a single sort:
SELECT id,
ROW_NUMBER() OVER (ORDER BY id) AS rn,
RANK() OVER (ORDER BY id) AS rk,
DENSE_RANK() OVER (ORDER BY id) AS dr
FROM $planets;Ranking Over a Join
A ranking window may read from a join, a CTE or a derived table:
SELECT p.name, ROW_NUMBER() OVER (ORDER BY p.id) AS rn
FROM $planets AS p
INNER JOIN testdata.satellites AS s
ON p.id = s.planetId;This is not true of aggregate windows, which accept only a single relation — see One Relation per Window.
Navigation Functions
SELECT <navigation_function>(<column> [, <offset>]) OVER ( [ PARTITION BY <column> ] ORDER BY <column> [ ASC | DESC ] )
FROM <relation_name>;Navigation functions return a value from another row of the partition, in the window's ORDER BY order. The result's type is the argument's type — LAG(name) over a VARCHAR column is VARCHAR.
Parameters
<navigation_function>:LAG(expr)—expr's value from the previous row;LAG(expr, offset)from the rowoffsetrows earlier.LEAD(expr)—expr's value from the next row;LEAD(expr, offset)from the rowoffsetrows later.
- The offset must be a non-negative integer literal, and defaults to 1. Offset 0 is the current row.
PARTITION BY <column>— optional. Navigation never crosses a partition edge: a row closer to the edge than the offset returnsNULL.ORDER BY <column>— required, exactly as for the ranking functions.
Examples
Previous and Next Row
SELECT name,
LAG(name) OVER (ORDER BY id) AS prev,
LEAD(name) OVER (ORDER BY id) AS next
FROM $planets;
-- Mercury's prev is NULL; Pluto's next is NULLPer-Partition, with an Explicit Offset
SELECT name, LAG(id, 2) OVER (PARTITION BY planetId ORDER BY id) AS l2
FROM testdata.satellites;
-- the first two rows of each partition carry NULLThe Argument May Be an Expression
SELECT name, LAG(mass * 2) OVER (ORDER BY id) AS prev_double_mass
FROM $planets;Notes
- The 3-argument default form —
LAG(expr, offset, default)— is not supported. Wrap the result instead:COALESCE(LAG(expr, offset), default). IGNORE NULLS/RESPECT NULLSare not supported.- Rows that tie on the
ORDER BYkey sit in an unspecified order, so over a non-totalORDER BYthe neighbouring row — and therefore the answer — is not deterministic between runs. The same caveat asROW_NUMBER, for the same reason.
Aggregate Windows
SELECT <aggregate_function>(<column>) OVER ( [ PARTITION BY <column> ] )
FROM <relation_name>;Which Aggregates Work
Rather than a short hand-maintained list, the answer is derivable from the engine's own aggregate
catalog. Every aggregate carries a support object — shown as the Support: line of its entry on
the Aggregates page — and two of its flags are exactly the two
window forms:
support flag |
Window form it governs |
|---|---|
grouped |
OVER (PARTITION BY <column>) |
global |
OVER () |
So an aggregate whose Support: line reads global, grouped works in both window forms; one that
reads only grouped works with PARTITION BY and is refused with OVER ().
At present every aggregate Opteryx implements has grouped: true, so any of them may be used
with PARTITION BY:
ANY_VALUE, APPROX_COUNT_DISTINCT, APPROX_PERCENTILE, ARRAY_AGG, AVG, CIDR_AGG, CORR, COUNT, COUNT_DISTINCT, MAX, MEDIAN, MIN, STDDEV, STDDEV_POP, STDDEV_SAMP, SUM, VAR_POP, VAR_SAMP
Two of them — ANY_VALUE and ARRAY_AGG — have global: false, and are refused with OVER (). See ARRAY_AGG and ANY_VALUE Need a Partition.
If this page and the catalog ever disagree, the catalog is right — it is generated from the engine's registrars. make check-sql fails when the two drift apart, so this list cannot silently go stale the way "SUM, COUNT, or AVG" did.
Parameters
<aggregate_function>— any aggregate from the catalog above, applied per-partition without collapsing rows. It is the same function, and the same result, you would get fromGROUP BY— attached to every row instead of one row per group.PARTITION BY <column>— optional. The partition key does not have to be unique. Omitting it entirely (OVER ()) makes the whole relation one window.
Examples
Basic Aggregate Window
SELECT name, SUM(gravity) OVER (PARTITION BY id)
FROM $planets;The Whole Relation as One Window
OVER () — empty parentheses, no partition key — computes the aggregate across every surviving row and attaches it to all of them:
SELECT name, COUNT(*) OVER () AS c, AVG(mass) OVER () AS a
FROM $planets;
-- every row carries c = 9 and a = 296.29184444444445If no rows survive, there are no rows to attach the aggregate to, and the result is zero rows — not one row holding a zero or a NULL:
SELECT name, COUNT(*) OVER () AS c
FROM $planets
WHERE id > 99;
-- 0 rowsMultiple Window Expressions
Multiple window expressions can appear in the same query, can be aliased, and may use different partition keys:
SELECT name,
COUNT(*) OVER (PARTITION BY number_of_moons) AS a,
SUM(mass) OVER (PARTITION BY id) AS b
FROM $planets;Non-Unique Partition Key
The partition key does not have to be unique — this counts satellites per planet, on every row:
SELECT name, COUNT(name) OVER (PARTITION BY planetId)
FROM testdata.satellites;Over a CTE or Derived Table
The window's input may be a CTE or a derived table rather than a named relation:
WITH x AS (SELECT * FROM $planets)
SELECT name, COUNT(*) OVER (PARTITION BY number_of_moons) AS c
FROM x;
SELECT name, COUNT(*) OVER (PARTITION BY number_of_moons) AS c
FROM (SELECT * FROM $planets) AS s;A CTE is one relation however many tables its body joins, so this is also the way to run an aggregate window over joined data:
WITH x AS (
SELECT p.name, s.planetId
FROM $planets AS p
INNER JOIN testdata.satellites AS s
ON p.id = s.planetId
)
SELECT name, COUNT(*) OVER (PARTITION BY planetId) AS c
FROM x;Notes
An
ORDER BYinside theOVERclause of an aggregate window is rejected —SUM(gravity) OVER (PARTITION BY id ORDER BY id)raisesWindow functions with ORDER BY are not supported. Use PARTITION BY only.This is the opposite of the ranking functions above, whereORDER BYis required. Running totals and moving averages, which are what that syntax would express, are not available.An aggregate window cannot be combined with
GROUP BYin the same query:SELECT id, SUM(gravity) OVER (PARTITION BY id) FROM $planets GROUP BY id; -- Window functions cannot be combined with GROUP BY.Row order is not preserved. An aggregate window with
PARTITION BYreturns rows grouped by partition, in an order that is not stable between runs of the same query. Add the statement's ownORDER BYif you need a defined order.
ARRAY_AGG and ANY_VALUE Need a Partition
ARRAY_AGG and ANY_VALUE work with OVER (PARTITION BY ...) but are refused with OVER ():
SELECT name, ARRAY_AGG(mass) OVER (PARTITION BY number_of_moons) AS s
FROM $planets; -- works
SELECT name, ARRAY_AGG(mass) OVER () AS s
FROM $planets;
-- ARRAY_AGG requires a GROUP BY clause, and cannot GROUP BY a literal value.This is consistent with the rest of the engine rather than a window-specific quirk: plain
SELECT ARRAY_AGG(mass) FROM $planets is refused with exactly the same message, and OVER ()
lowers to that same ungrouped aggregate. It is the support.global: false flag in the catalog,
showing through.
One Relation per Window
An aggregate window reads from exactly one relation. Two relations directly beneath it — a join, or a CROSS JOIN against a VALUES list — are refused:
SELECT p.name, COUNT(*) OVER (PARTITION BY p.id) AS c
FROM $planets AS p
INNER JOIN testdata.satellites AS s
ON p.id = s.planetId;
-- Window functions over multiple joined tables are not yet supported.
-- Compute the window in a subquery over a single relation, then join to that result.Wrapping the join in a CTE or derived table satisfies the rule, because the window then sees one
relation — see Over a CTE or Derived Table. The window's input
must also be a real relation: an inline VALUES list is rejected with Window functions require a base table.
Ranking functions do not have this restriction.
Restrictions on Both Families
These apply to ranking functions and aggregate windows alike.
A Window May Sit Inside a Larger Expression
A window function does not have to be the whole projection column — it may be one operand of a larger expression, an argument to a function, or the target of a cast, and both families may:
SELECT name, mass / SUM(mass) OVER () AS pct FROM $planets; -- percent-of-total
SELECT name, ROW_NUMBER() OVER (ORDER BY id) + 1 AS rn FROM $planets;
SELECT CAST(COUNT(*) OVER () AS VARCHAR) FROM $planets;The window is computed first and the rest of the expression is evaluated over its output, one row at a time, so the nested form answers exactly what the un-nested one does. Base columns may be named in the same expression (mass / SUM(mass) OVER ()), and several windows may appear in one expression, over the same spec or different ones. Placement is the SELECT list or QUALIFY.
Two enclosing contexts remain refused, each with its own remedy:
Inside an aggregate's argument —
SUM(COUNT(*) OVER ()),MAX(ROW_NUMBER() OVER (ORDER BY id)), or with the window only part of the argument (SUM(mass + COUNT(*) OVER ())). Standard SQL forbids it outright. Compute the window in a subquery, then aggregate its result:SELECT SUM(x) FROM (SELECT COUNT(*) OVER () AS x FROM $planets) AS t;Inside another window — whether in its argument (
SUM(COUNT(*) OVER ()) OVER ()) or in itsOVERspec (SUM(mass) OVER (PARTITION BY COUNT(*) OVER ())). Chain the windows across a subquery boundary instead — every combination of the two families runs this way, and nests further than one level deep:SELECT SUM(r) OVER () FROM (SELECT ROW_NUMBER() OVER (ORDER BY id) AS r FROM $planets) AS t;
SELECT * Cannot Be Combined with a Window
SELECT *, COUNT(*) OVER () AS c FROM $planets;
-- `SELECT *` cannot coexist with additional columns. List the columns you want
-- explicitly, or use `SELECT *` on its own.This is a blanket Opteryx rule and not window-specific — SELECT *, id + 1 FROM $planets fails identically. It does differ from standard SQL, DuckDB and PostgreSQL, all of which allow it.
The qualified wildcard is the escape hatch, and is the form to reach for here:
SELECT p.*, COUNT(*) OVER () AS c
FROM $planets AS p;
-- 21 columns: the 20 from $planets, plus cUnaliased Windows Are Named for Their Expression
A window function with no alias is named for the expression it renders to:
SELECT name, COUNT(*) OVER () FROM $planets;
-- columns: name, "COUNT(*) OVER ()"A consequence worth planning around: two unaliased windows that render to the same text are two columns with one name, and the query is rejected as ambiguous — exactly as a repeated unaliased expression would be.
SELECT name, COUNT(*) OVER (), COUNT(*) OVER () FROM $planets;
-- AmbiguousIdentifierError: Query result contains multiple instances of the
-- same column(s) - `COUNT(*) OVER ()`Alias them, and the ambiguity goes away.
Filtering on a Window
WHERE runs before the window is computed, so it cannot filter on a window function's output — a window function in a WHERE clause fails with an internal error rather than a diagnosable one. QUALIFY is the clause that can; it is to window functions what HAVING is to GROUP BY:
SELECT name FROM $planets QUALIFY COUNT(*) OVER () > 5;
SELECT name FROM $planets QUALIFY ROW_NUMBER() OVER (ORDER BY id) <= 3;Every restriction on this page applies to a window inside a QUALIFY as well. See QUALIFY for its own rules — chiefly that the condition must contain a window function, and that a SELECT alias for one is not accepted.
Notes
- Window functions are evaluated after
WHERE,GROUP BY, andHAVING, but before the statement's ownORDER BY. A filtered-out row does not contribute to the window:SELECT name, COUNT(*) OVER () AS c FROM $planets WHERE id > 6returns 3 rows, each withc = 3. - Each row in the result retains its own values — rows are not collapsed as they are with plain aggregation.
- Multiple window expressions with different
PARTITION BYcolumns can appear in the sameSELECT. - Frame specification (
ROWS BETWEEN,RANGE BETWEEN) is not supported for either family. NTILE,FIRST_VALUE,LAST_VALUE,NTH_VALUE, and namedWINDOWclauses are not implemented.
See Also
- SQL Conformance — how this compares to the standard
- Aggregates — the same functions used with
GROUP BY, and thesupportflags this page derives its list from - QUALIFY — filtering on a window function's result
- GROUP BY
- HAVING
- SELECT
- ORDER BY