Use this file to discover all available pages before exploring further.
Window functions perform calculations across a set of rows that are related to the current row. Unlike aggregate functions that collapse rows, window functions retain all rows while adding computed values.
-- Leaves gaps after tiesSELECT salary, RANK() OVER (ORDER BY salary DESC) AS rankFROM empsalary;-- Same salary gets same rank, next rank skips
Example from DuckDB tests showing DENSE_RANK:
WITH t AS ( SELECT i, DENSE_RANK() OVER (ORDER BY i % 50) AS d FROM range(3000) tbl(i))SELECT COUNT(*) AS num_ranks, MIN(d) AS min_rank, MAX(d) AS max_rankFROM ( SELECT d, COUNT(*) AS c FROM t GROUP BY d) w;
From DuckDB’s test suite - counting distinct values:
WITH dups AS ( SELECT ROW_NUMBER() OVER same_idx AS dup, COUNT(*) OVER same_idx AS n_dup, (DENSE_RANK() OVER asc_spcmn) + (DENSE_RANK() OVER desc_spcmn) - 1 AS n_spcmn, * FROM issue9416 WINDOW same_idx AS ( PARTITION BY idx ORDER BY source, project, specimen ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ), asc_spcmn AS ( PARTITION BY idx ORDER BY specimen ASC NULLS FIRST ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ), desc_spcmn AS ( PARTITION BY idx ORDER BY specimen DESC NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ))SELECT *FROM dupsWHERE n_spcmn > 1ORDER BY idx, dup;
-- Access next row's valueSELECT date, amount, LEAD(amount) OVER (ORDER BY date) AS next_amount, LEAD(amount, 2) OVER (ORDER BY date) AS amount_two_aheadFROM sales;
Real example from DuckDB tests:
CREATE TABLE win( id INT, v INT, t INT, f FLOAT, s VARCHAR);INSERT INTO win VALUES (1, 1, 2, 0.54, 'h'), (1, 1, 1, 0.21, 'e'), (1, 2, 3, 0.001, 'l'), (2, 10, 4, 0.04, 'l'), (2, 11, -1, 10.45, 'o');SELECT id, v, t, LAG(v, 2, NULL) OVER (PARTITION BY id ORDER BY t ASC) AS laggedFROM winORDER BY id, t;
SELECT date, "group", count, status, LEAD(date, 2) OVER ( PARTITION BY "group" ORDER BY date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS end_date, LEAD(status, 2) OVER ( PARTITION BY "group" ORDER BY date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS end_statusFROM issue14398ORDER BY "group", date;
SELECT depname, empno, salary, FIRST_VALUE(empno) OVER ( PARTITION BY depname ORDER BY empno ) AS first_empnoFROM empsalary;
LAST_VALUE requires careful frame specification. By default, frames end at the current row, so you typically need ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
Any aggregate function can be used as a window function:
SELECT depname, salary, SUM(salary) OVER (PARTITION BY depname ORDER BY salary) AS running_sum, AVG(salary) OVER (PARTITION BY depname ORDER BY salary) AS running_avg, COUNT(*) OVER (PARTITION BY depname ORDER BY salary) AS running_countFROM empsalaryORDER BY depname, salary;
-- Physical offset in rowsSELECT date, amount, AVG(amount) OVER ( ORDER BY date ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING ) AS moving_avg_5dayFROM sales;
-- Default: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWSELECT SUM(salary) OVER (ORDER BY empno);-- Explicit boundariesSELECT salary, SUM(salary) OVER ( ORDER BY empno ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS three_row_sum, SUM(salary) OVER ( ORDER BY empno ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS total_sum, SUM(salary) OVER ( ORDER BY empno ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ) AS remaining_sumFROM empsalary;
SELECT depname, empno, salary, ROW_NUMBER() OVER w AS row_num, AVG(salary) OVER w AS dept_avg, salary - AVG(salary) OVER w AS diffFROM empsalaryWINDOW w AS (PARTITION BY depname ORDER BY salary)ORDER BY depname, salary;
WITH ranked AS ( SELECT depname, empno, salary, ROW_NUMBER() OVER (PARTITION BY depname ORDER BY salary DESC) AS rank FROM empsalary)SELECT depname, empno, salaryFROM rankedWHERE rank <= 3ORDER BY depname, rank;
SELECT date, price, AVG(price) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS moving_avg_7day, AVG(price) OVER ( ORDER BY date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) AS moving_avg_30dayFROM stock_pricesORDER BY date;
SELECT DATE_TRUNC('month', date) AS month, SUM(amount) AS monthly_total, LAG(SUM(amount), 12) OVER (ORDER BY DATE_TRUNC('month', date)) AS last_year, SUM(amount) - LAG(SUM(amount), 12) OVER (ORDER BY DATE_TRUNC('month', date)) AS yoy_changeFROM salesGROUP BY monthORDER BY month;
SELECT empno, salary, PERCENT_RANK() OVER (ORDER BY salary) AS percentile, CUME_DIST() OVER (ORDER BY salary) AS cumulative_dist, NTILE(4) OVER (ORDER BY salary) AS quartileFROM empsalary;
Window functions are evaluated after WHERE, GROUP BY, and HAVING but before ORDER BY and LIMIT.