Aggregate and Window Functions
|
This section documents standard SQL (ISO/IEC 9075-2 (SQL/Foundation)) functionality only. Vendor-specific extensions or behavior of any particular database management system are intentionally left out. In the US, the standard is also distributed via ANSI’s webstore (search "ISO/IEC 9075"). This content was generated with the assistance of AI and should be verified against your target DBMS’s own documentation before relying on it in production. |
Aggregate functions collapse a set of rows into a single summary value, typically in combination with
GROUP BY. Window functions, introduced by the SQL:2003 standard, compute a value for every input row while
still having access to a set of related rows — the row’s "window" — without collapsing the result set. This
page covers both, and shows how the same aggregate functions can be reused as window functions via the
OVER clause. For the authoritative (if less readable) specification, see
ISO/IEC 9075-2 (SQL/Foundation), the part of the standard that defines
aggregate and window functions.
The examples below assume a table of orders:
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
region VARCHAR(20) NOT NULL,
order_date DATE NOT NULL,
amount NUMERIC(10, 2) NOT NULL,
discount_amount NUMERIC(10, 2)
);
Aggregate Functions
Aggregate functions take a collection of rows (the whole table, or one group produced by GROUP BY) and
return a single value. Except for COUNT(*), all aggregate functions ignore NULL values in their input.
COUNT
COUNT returns the number of rows in a group. It has three forms:
-
COUNT(*)— counts every row in the group, including rows that containNULLvalues in any column. -
COUNT(column)— counts only the rows wherecolumnis notNULL. -
COUNT(DISTINCT column)— counts the number of distinct, non-NULLvalues ofcolumn.
SELECT
COUNT(*) AS total_rows,
COUNT(discount_amount) AS rows_with_discount,
COUNT(DISTINCT customer_id) AS distinct_customers
FROM orders;
SUM
SUM(expression) returns the total of a numeric expression across the rows in a group, ignoring NULL
values. It returns NULL if every value in the group is NULL or the group is empty.
SELECT region, SUM(amount) AS region_total
FROM orders
GROUP BY region;
AVG
AVG(expression) returns the arithmetic mean of a numeric expression across the rows in a group, ignoring
NULL values.
SELECT region, AVG(amount) AS region_average
FROM orders
GROUP BY region;
Window Functions
A window function computes a value for each row using a set of rows related to that row, but — unlike
GROUP BY — it does not reduce the number of rows returned. Every window function call requires an OVER
clause, which defines the window: the rows visible to the function for each row of the result.
The OVER Clause
The general form is:
function_name(...) OVER (
[ PARTITION BY partition_expression [, ...] ]
[ ORDER BY sort_expression [ASC | DESC] [, ...] ]
)
-
PARTITION BYdivides the rows into independent groups (partitions). The window function is evaluated separately within each partition, restarting for every new partition — much likeGROUP BY, except rows are not collapsed. OmittingPARTITION BYtreats the entire result set as a single partition. -
ORDER BYdefines the logical order of rows within each partition. It determines row order for order-sensitive functions such asROW_NUMBER,RANK, andLAG/LEAD, and, for aggregate functions used as window functions, it also determines the running/cumulative frame (see theSUM() OVERexample below). OmittingORDER BYmeans there is no defined row order within the partition.
SELECT
customer_id,
region,
amount,
order_date
FROM orders
ORDER BY region, customer_id;
The subsections below apply this OVER (PARTITION BY … ORDER BY …) clause to specific functions.
ROW_NUMBER
ROW_NUMBER() returns a unique, sequential integer for each row within its partition, starting at 1 and
following the order given by ORDER BY. It takes no parameters. Because it is strictly sequential, ties in
the ORDER BY expression are broken arbitrarily — no two rows in the same partition ever get the same
number.
SELECT
customer_id,
region,
amount,
ROW_NUMBER() OVER (
PARTITION BY region
ORDER BY amount DESC
) AS row_num
FROM orders;
RANK
RANK() returns the position of each row within its partition, according to the order given by ORDER BY.
Rows with equal values in the ORDER BY expression receive the same rank, and the next rank after a tie is
skipped by the number of tied rows (e.g. after two rows tied at rank 1, the next row gets rank 3).
SELECT
customer_id,
region,
amount,
RANK() OVER (
PARTITION BY region
ORDER BY amount DESC
) AS amount_rank
FROM orders;
DENSE_RANK
DENSE_RANK() behaves like RANK(), assigning the same rank to tied rows, but it never skips a rank
value: the rank after a tie is always one greater than the previous rank, regardless of how many rows were
tied.
SELECT
customer_id,
region,
amount,
DENSE_RANK() OVER (
PARTITION BY region
ORDER BY amount DESC
) AS amount_dense_rank
FROM orders;
LAG and LEAD
LAG and LEAD look at a row that comes before (LAG) or after (LEAD) the current row, within the same
partition, according to the order given by ORDER BY.
LAG(expression [, offset [, default_value]]) OVER (...)
LEAD(expression [, offset [, default_value]]) OVER (...)
-
expression— the column or expression to read from the offset row. -
offset— how many rows before (LAG) or after (LEAD) the current row to look at. Defaults to1when omitted. -
default_value— the value returned when the offset row falls outside the partition (e.g.LAGon the first row, orLEADon the last row). Defaults toNULLwhen omitted.
SELECT
customer_id,
order_date,
amount,
LAG(amount, 1, 0) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS previous_order_amount,
LEAD(amount, 1, 0) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS next_order_amount
FROM orders;
NTILE
NTILE(n) divides the rows of each partition, in the order given by ORDER BY, into n roughly
equal-sized buckets, and returns the bucket number (from 1 to n) for each row. When the number of rows
in a partition is not evenly divisible by n, the earlier buckets receive one extra row each.
SELECT
customer_id,
amount,
NTILE(4) OVER (
ORDER BY amount DESC
) AS amount_quartile
FROM orders;
Aggregate Functions as Window Functions
Any aggregate function — COUNT, SUM, AVG, MIN, MAX — can be used as a window function simply by
adding an OVER clause instead of a GROUP BY clause. This preserves one output row per input row while
still producing group-level or running totals alongside the detail data.
SELECT
customer_id,
region,
order_date,
amount,
-- total for the whole partition (no ORDER BY inside OVER)
SUM(amount) OVER (
PARTITION BY region
) AS region_total,
-- running total within each customer, ordered by date
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS running_customer_total,
AVG(amount) OVER (
PARTITION BY region
) AS region_average
FROM orders;