Window functions are the most powerful feature in modern SQL. They perform calculations across a set of rows related to the current row — without collapsing them into groups like GROUP BY does. You get both the detail rows and the aggregate in the same result.
The OVER() Clause
Every window function uses OVER() to define the "window" of rows it operates on:
SQL
SELECT
name, department, salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY department) AS diff_from_avg
FROM employees;
Result
name | department | salary | dept_avg | diff_from_avg
--------------|-------------|---------|-----------|-------------
Sara Ahmed | Engineering | 95000 | 96000.00 | -1000.00
James Wilson | Engineering | 88000 | 96000.00 | -8000.00
David Chen | Engineering | 105000 | 96000.00 | 9000.00
Priya Sharma | Marketing | 72000 | 70000.00 | 2000.00
Emma Johnson | Marketing | 68000 | 70000.00 | -2000.00
Ali Hassan | Sales | 78000 | 80000.00 | -2000.00
Maria Garcia | Sales | 82000 | 80000.00 | 2000.00
Notice: no GROUP BY. Every row is preserved. The department average appears next to each employee. This is impossible with regular aggregate functions.
ROW_NUMBER, RANK, DENSE_RANK
SQLSELECT
name, department, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank_all,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;
Result
name | department | salary | row_num | rank_all | dept_rank
--------------|-------------|---------|---------|----------|----------
David Chen | Engineering | 105000 | 1 | 1 | 1
Sara Ahmed | Engineering | 95000 | 2 | 2 | 2
James Wilson | Engineering | 88000 | 3 | 3 | 3
Maria Garcia | Sales | 82000 | 4 | 4 | 1
Ali Hassan | Sales | 78000 | 5 | 5 | 2
Priya Sharma | Marketing | 72000 | 6 | 6 | 1
Emma Johnson | Marketing | 68000 | 7 | 7 | 2
| Function | Ties | Gap After Tie | Use Case |
ROW_NUMBER() | No ties — always unique | N/A | Pagination, deduplication |
RANK() | Same rank for ties | Yes (1, 2, 2, 4) | Competition rankings |
DENSE_RANK() | Same rank for ties | No (1, 2, 2, 3) | Top-N per group |
LEAD and LAG: Look at Adjacent Rows
SQL
SELECT
name, hire_date, salary,
LAG(salary) OVER (ORDER BY hire_date) AS prev_salary,
LEAD(salary) OVER (ORDER BY hire_date) AS next_salary,
salary - LAG(salary) OVER (ORDER BY hire_date) AS change
FROM employees;
Result
name | hire_date | salary | prev_salary | next_salary | change
--------------|------------|--------|-------------|-------------|-------
David Chen | 2019-06-20 | 105000 | NULL | 95000 | NULL
Sara Ahmed | 2020-03-15 | 95000 | 105000 | 82000 | -10000
Maria Garcia | 2020-09-01 | 82000 | 95000 | 88000 | -13000
James Wilson | 2021-07-01 | 88000 | 82000 | 78000 | 6000
Ali Hassan | 2021-11-15 | 78000 | 88000 | 72000 | -10000
Priya Sharma | 2022-01-10 | 72000 | 78000 | 68000 | -6000
Emma Johnson | 2023-03-01 | 68000 | 72000 | NULL | -4000
Running Totals with SUM OVER
SQL
SELECT
name, hire_date, salary,
SUM(salary) OVER (ORDER BY hire_date) AS running_total
FROM employees;
Window functions are interview favorites. "Get the top 2 earners per department" is a classic SQL interview question. Answer: WITH ranked AS (SELECT *, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rk FROM employees) SELECT * FROM ranked WHERE rk <= 2
Key Takeaways
- Window functions add aggregate/ranking columns without collapsing rows
-
OVER (PARTITION BY col ORDER BY col) defines the window
-
ROW_NUMBER for unique numbering; RANK/DENSE_RANK for rankings
-
LAG/LEAD to compare with previous/next rows
-
SUM() OVER (ORDER BY ...) for running totals