Window Functions
What Are Window Functions?
GROUP BY collapses rows into one row per group. Window functions add an extra computed column to every row while keeping all rows visible.
-- Without window function (collapses rows) SELECT grade, AVG(age) FROM students GROUP BY grade; -- With window function (keeps all rows + adds aggregate column) SELECT name, grade, age, AVG(age) OVER (PARTITION BY grade) AS avg_age_in_grade FROM students;
The OVER clause turns any aggregate function into a window function. PARTITION BY defines the group (partition), but all rows are still returned.
PARTITION BY and ORDER BY in OVER
PARTITION BY splits rows into partitions (like GROUP BY, but without collapsing). ORDER BY inside OVER defines the order for ranking or running calculations.
SELECT name, grade, age, ROW_NUMBER() OVER (PARTITION BY grade ORDER BY age DESC) AS rank_in_grade FROM students;
Each student gets a rank within their grade group, ordered by age descending. The partition restarts the counter for each new grade.
Ranking Functions
SQL provides four ranking functions that differ in how they handle ties:
SELECT name, score, ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num, -- unique rank RANK() OVER (ORDER BY score DESC) AS rank_val, -- gaps for ties DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank, -- no gaps NTILE(4) OVER (ORDER BY score DESC) AS quartile -- divide into 4 groups FROM scores;
LAG and LEAD — Row Comparisons
LAG(col, n, default) looks back n rows; LEAD(col, n, default) looks forward n rows. Perfect for period-over-period comparisons.
SELECT order_date, revenue, LAG(revenue, 1, 0) OVER (ORDER BY order_date) AS prev_revenue, LEAD(revenue, 1, 0) OVER (ORDER BY order_date) AS next_revenue, revenue - LAG(revenue, 1, 0) OVER (ORDER BY order_date) AS change FROM monthly_sales;
The third argument to LAG/LEAD is a default value returned when there is no previous/next row (e.g., the first or last row).
Running Totals with SUM OVER
Adding ORDER BY inside SUM() OVER creates a running (cumulative) total. You can also define a row frame for moving averages.
SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date) AS running_total, AVG(amount) OVER (ORDER BY order_date ROWS 6 PRECEDING) AS `7day_avg` FROM daily_sales;