What are window functions in SQL?
Window functions perform calculations across a set of related rows without collapsing them into groups - like aggregate functions but each row keeps its identity.
SQLPostgreSQL
Overview
Window functions (also called analytic functions) calculate values across a "window" of rows related to the current row. Unlike `GROUP BY`, the rows are not collapsed - you get one output row per input row. They are used for rankings, running totals, moving averages, and comparing rows to their neighbours.
Key Points
- •Syntax: `function() OVER (PARTITION BY ... ORDER BY ...)`.
- •`PARTITION BY`: splits rows into groups (like GROUP BY but without collapsing).
- •`ORDER BY` inside OVER: defines row order for running totals, rankings, etc.
- •Common functions: `ROW_NUMBER()`, `RANK()`, `DENSE_RANK()`, `LAG()`, `LEAD()`, `SUM() OVER`, `AVG() OVER`.
- •`LAG(col, n)` accesses the value n rows before the current row; `LEAD` goes forward.
- •Window functions run after WHERE/GROUP BY/HAVING but before ORDER BY/LIMIT.
Examples
ROW_NUMBER: assign a rank within each department
sqlSELECT
name,
department,
salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS rank_in_dept
FROM employees;
-- Each department gets its own rank sequence: 1, 2, 3...
-- The rows are not collapsed - every employee appearsSUM OVER: running total of sales
sqlSELECT
sale_date,
amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM sales;
-- sale_date | amount | running_total
-- 2025-01-01| 100 | 100
-- 2025-01-02| 250 | 350
-- 2025-01-03| 80 | 430LAG and LEAD: compare to previous/next row
sqlSELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
revenue - LAG(revenue, 1) OVER (ORDER BY month) AS month_over_month_change
FROM monthly_revenue;RANK vs DENSE_RANK vs ROW_NUMBER on ties
sqlSELECT score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM scores;
-- score | row_num | rank | dense_rank
-- 100 | 1 | 1 | 1
-- 90 | 2 | 2 | 2
-- 90 | 3 | 2 | 2 ← tie
-- 80 | 4 | 4 | 3 ← RANK skips 3; DENSE_RANK does notPro Tip
Window functions unlock analysis that would require self-joins or subqueries otherwise. If you need "the top N per group", "running totals", or "compare each row to the previous one", reach for a window function - it will be cleaner and more performant.