Skip to main content
Version: 0.3.36

Window Functions

A window function computes a value for each row from the rows around it, without collapsing the result the way GROUP BY does. It is written as function(...) OVER (...).

function(...) OVER (
[ PARTITION BY expression [, ...] ]
[ ORDER BY expression [ ASC | DESC ] [, ...] ]
[ ROWS BETWEEN frame_start AND frame_end ]
)

Available today: the ranking functions ROW_NUMBER(), RANK() and DENSE_RANK(), and the aggregates COUNT, SUM, AVG, MIN, MAX. LAG, LEAD, FIRST_VALUE, LAST_VALUE and NTILE are rejected by the analyzer (Window function LAG is not supported).

The examples use the blog workspace: hello (depth 1, page), news (depth 1, folder), first and second (depth 2, pages).

ROW_NUMBER

Sequential number of the row within its partition, in the window's ORDER BY.

ROW_NUMBER() OVER (...)BIGINT
SELECT name, ROW_NUMBER() OVER (ORDER BY name) AS rn FROM 'blog' ORDER BY name;
-- {"name":"first","rn":1}, {"name":"hello","rn":2}, {"name":"news","rn":3}, {"name":"second","rn":4}

SELECT name, node_type, ROW_NUMBER() OVER (PARTITION BY node_type ORDER BY name) AS rn
FROM 'blog' ORDER BY node_type, name;
{"name":"news","node_type":"raisin:Folder","rn":1}
{"name":"first","node_type":"raisin:Page","rn":1}
{"name":"hello","node_type":"raisin:Page","rn":2}
{"name":"second","node_type":"raisin:Page","rn":3}

Top-N per group is a subquery over ROW_NUMBER(); a window function cannot appear in WHERE directly:

SELECT * FROM (
SELECT name, node_type, ROW_NUMBER() OVER (PARTITION BY node_type ORDER BY name) AS rn FROM 'blog'
) ranked WHERE rn <= 1;
-- {"name":"news","node_type":"raisin:Folder","rn":1}, {"name":"first","node_type":"raisin:Page","rn":1}

Aggregates over a window

COUNT, SUM, AVG, MIN and MAX with OVER keep every row and attach the aggregate of the row's frame.

SELECT name, node_type,
COUNT(*) OVER () AS total,
COUNT(*) OVER (PARTITION BY node_type) AS per_type,
AVG(depth) OVER (PARTITION BY node_type) AS avg_depth,
MAX(depth) OVER (PARTITION BY node_type) AS max_depth
FROM 'blog' ORDER BY node_type, name;
{"name":"news","node_type":"raisin:Folder","total":4,"per_type":1,"avg_depth":1.0,"max_depth":1}
{"name":"first","node_type":"raisin:Page","total":4,"per_type":3,"avg_depth":1.6666666666666667,"max_depth":2}
{"name":"hello","node_type":"raisin:Page","total":4,"per_type":3,"avg_depth":1.6666666666666667,"max_depth":2}
{"name":"second","node_type":"raisin:Page","total":4,"per_type":3,"avg_depth":1.6666666666666667,"max_depth":2}

Frames

A ROWS BETWEEN clause picks which rows of the partition, relative to the current one, feed the aggregate. Bounds are UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING and UNBOUNDED FOLLOWING.

SELECT name, depth,
SUM(depth) OVER (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running,
AVG(depth) OVER (ORDER BY name ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS centered
FROM 'blog' ORDER BY name;
{"name":"first","depth":2,"running":2,"centered":1.5}
{"name":"hello","depth":1,"running":3,"centered":1.3333333333333333}
{"name":"news","depth":1,"running":4,"centered":1.3333333333333333}
{"name":"second","depth":2,"running":6,"centered":1.5}

When no frame is given, the aggregate covers the whole partition, with or without ORDER BY. That differs from PostgreSQL, where an ORDER BY alone implies a running frame. Write the frame out when you want a running total:

SELECT name, depth, SUM(depth) OVER (ORDER BY name) AS whole FROM 'blog' ORDER BY name;
-- every row: "whole": 6

RANGE BETWEEN ... is accepted and treated the same as ROWS BETWEEN ... (bounds count rows, not values).

PARTITION BY and ORDER BY

PARTITION BY splits the rows into independent groups; ORDER BY inside OVER sets the numbering order and the frame order and may list several expressions with ASC / DESC. Both accept any expression, including JSON accesses:

SELECT properties->>'title' AS title,
ROW_NUMBER() OVER (PARTITION BY properties->>'published' ORDER BY (properties->>'views')::INT DESC) AS rank_in_group
FROM 'blog' WHERE node_type = 'raisin:Page';

The outer query's ORDER BY is separate from the window's; add one when the output order matters.

Notes

  • Several window functions can share one statement, each with its own OVER.
  • Window functions run after WHERE and GROUP BY; to filter on their result, wrap the query in a subquery.
  • A window can be applied over a GROUP BY result, which gives percentages of a total in one statement:
SELECT node_type, COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () AS pct FROM 'blog' GROUP BY node_type;
-- {"node_type":"raisin:Folder","pct":25.0}, {"node_type":"raisin:Page","pct":75.0}