SQL Tutorial › Window Functions › Window Functions

SQL Window Functions

AdvancedLesson 45 of 6914 min read9 queries run on MySQL 8.4

A window function calculates a value across a set of related rows (the window) without collapsing them into one row the way GROUP BY does. You keep every employee row and still see the department average, the rank inside the department or the running total. Window functions are the most asked advanced topic in SQL interviews, and MySQL supports them from version 8.0.

Advertisement
In this lesson
  • How OVER() differs from GROUP BY
  • Split rows into windows with PARTITION BY
  • Order a window with ORDER BY for running results
  • Reuse a window with WINDOW and control the frame

GROUP BY vs window function

GROUP BY returns one row per department:

SQL
SELECT dept_id, ROUND(AVG(salary)) AS dept_avgFROM employeesWHERE dept_id IN (1, 2)GROUP BY dept_id;
ResultMySQL 8.4
dept_iddept_avg
178400
269000
2 rows in set

A window function keeps every row and adds the department average to each one:

SQL
SELECT first_name, dept_id, salary,       ROUND(AVG(salary) OVER (PARTITION BY dept_id)) AS dept_avgFROM employeesWHERE dept_id IN (1, 2)ORDER BY dept_id, salary DESC;
ResultMySQL 8.4
first_namedept_idsalarydept_avg
Asha112500078400
Rahul19800078400
Priya17200078400
Karan17200078400
Amit12500078400
Vikram210500069000
Neha25400069000
Arjun24800069000
8 rows in set
How this query works, step by step
  1. OVER (...) turns AVG into a window function.
  2. PARTITION BY dept_id splits the rows into windows, one per department.
  3. For each row, AVG is computed over that row’s window: Engineering rows all get 78,400 and Sales rows 69,000.
  4. No row disappears: 8 rows in, 8 rows out.

Window function syntax

MySQL
function_name(args) OVER (  [PARTITION BY columns]  [ORDER BY columns]  [frame clause])
PartDoes
PARTITION BYdivides rows into independent groups; without it the whole result is one window
ORDER BYorders rows inside each window; needed for ranking, LAG/LEAD and running totals
framewhich rows around the current one to include, e.g. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idsalaryhire_date
100MeeraNULL2500002015-01-05
101Asha11250002018-04-12
102Rahul1980002019-07-01
103Priya1720002021-01-18
104Karan1720002022-08-29
105Vikram21050002017-11-20
106Neha2540002022-03-14
107Arjun2480002023-06-05
108Kavya3880002020-09-09
109Rohan3450002024-01-22
110Sneha4780002019-02-25
111Amit1250002025-06-02

OVER() with no partition

An empty OVER () makes the whole result one window. Each salary as a share of the total payroll:

MySQL
SELECT first_name, salary,       SUM(salary) OVER () AS total_payroll,       ROUND(100 * salary / SUM(salary) OVER (), 1) AS pct_of_payrollFROM employeesORDER BY salary DESCLIMIT 5;
ResultMySQL 8.4
first_namesalarytotal_payrollpct_of_payroll
Meera250000106000023.6
Asha125000106000011.8
Vikram10500010600009.9
Rahul9800010600009.2
Kavya8800010600008.3
5 rows in set
How this query works, step by step
  1. SUM(salary) OVER () is the sum of all 12 salaries, repeated on every row: 1,060,000.
  2. Dividing each salary by it gives its share.
  3. Window functions are computed before ORDER BY and LIMIT, so the total still covers all 12 rows even though only 5 are shown.

Ranking functions

SQL
SELECT first_name, dept_id, salary,       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS row_num,       RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,       DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dense_rnkFROM employeesWHERE dept_id = 1;
ResultMySQL 8.4
first_namedept_idsalaryrow_numrnkdense_rnk
Asha1125000111
Rahul198000222
Priya172000333
Karan172000433
Amit125000554
5 rows in set
How this query works, step by step
  1. ORDER BY salary DESC inside OVER ranks from the highest salary.
  2. Priya and Karan tie at 72,000: ROW_NUMBER still gives them 3 and 4, RANK gives both 3 and skips 4, DENSE_RANK gives both 3 without a gap.
  3. Full lesson: ROW_NUMBER, RANK, DENSE_RANK.

ORDER BY in the window: running totals

Adding ORDER BY to an aggregate’s window makes it cumulative:

SQL
SELECT first_name, hire_date, salary,       SUM(salary) OVER (ORDER BY hire_date) AS running_payroll,       COUNT(*)    OVER (ORDER BY hire_date) AS headcountFROM employeesORDER BY hire_date;
ResultMySQL 8.4
first_namehire_datesalaryrunning_payrollheadcount
Meera2015-01-052500002500001
Vikram2017-11-201050003550002
Asha2018-04-121250004800003
Sneha2019-02-25780005580004
Rahul2019-07-01980006560005
Kavya2020-09-09880007440006
Priya2021-01-18720008160007
Neha2022-03-14540008700008
Karan2022-08-29720009420009
Arjun2023-06-054800099000010
Rohan2024-01-2245000103500011
Amit2025-06-0225000106000012
12 rows in set
How this query works, step by step
  1. Rows are ordered by hire_date inside the window.
  2. For each row, SUM adds the salaries of everyone hired up to and including that date.
  3. COUNT(*) the same way gives the headcount over time. More in Running Totals.

Values from other rows: LAG and LEAD

SQL
SELECT first_name, hire_date,       LAG(first_name)  OVER (ORDER BY hire_date) AS hired_before,       LEAD(hire_date)  OVER (ORDER BY hire_date) AS next_hire_dateFROM employeesORDER BY hire_dateLIMIT 6;
ResultMySQL 8.4
first_namehire_datehired_beforenext_hire_date
Meera2015-01-05NULL2017-11-20
Vikram2017-11-20Meera2018-04-12
Asha2018-04-12Vikram2019-02-25
Sneha2019-02-25Asha2019-07-01
Rahul2019-07-01Sneha2020-09-09
Kavya2020-09-09Rahul2021-01-18
6 rows in set

Full lesson: LAG & LEAD.

Named windows

When several functions use the same window, define it once with WINDOW:

SQL
SELECT first_name, dept_id, salary,       MIN(salary) OVER w AS dept_min,       MAX(salary) OVER w AS dept_max,       salary - MIN(salary) OVER w AS above_minFROM employeesWHERE dept_id IS NOT NULLWINDOW w AS (PARTITION BY dept_id)ORDER BY dept_id, salary;
ResultMySQL 8.4
first_namedept_idsalarydept_mindept_maxabove_min
Amit125000250001250000
Priya1720002500012500047000
Karan1720002500012500047000
Rahul1980002500012500073000
Asha112500025000125000100000
Arjun248000480001050000
Neha254000480001050006000
Vikram21050004800010500057000
Rohan34500045000880000
Kavya388000450008800043000
Sneha47800078000780000
11 rows in set

Where window functions are allowed

Window functions run after WHERE, GROUP BY and HAVING, so they can only appear in SELECT and ORDER BY. To filter on one, wrap the query in a CTE or subquery:

MySQL
SELECT first_name, RANK() OVER (ORDER BY salary DESC) AS rnkFROM employeesWHERE rnk <= 3;
ResultMySQL 8.4
ERROR 1054 (42S22): Unknown column 'rnk' in 'where clause'
SQL
WITH ranked AS (  SELECT first_name, salary, RANK() OVER (ORDER BY salary DESC) AS rnk  FROM employees)SELECT * FROM ranked WHERE rnk <= 3;
ResultMySQL 8.4
first_namesalaryrnk
Meera2500001
Asha1250002
Vikram1050003
3 rows in set

Window functions in MySQL

GroupFunctions
RankingROW_NUMBER, RANK, DENSE_RANK, PERCENT_RANK, CUME_DIST, NTILE
ValueLAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE
AggregateSUM, AVG, COUNT, MIN, MAX (and others) with OVER

Exercise: Salary vs department maximum

Show each employee’s first_name, dept_id, salary and the gap to the highest salary in their department, for employees with a department.

Expected resultMySQL 8.4
first_namedept_idsalarygap_to_top
Asha11250000
Rahul19800027000
Priya17200053000
Karan17200053000
Amit125000100000
Vikram21050000
Neha25400051000
Arjun24800057000
Kavya3880000
Rohan34500043000
Sneha4780000
11 rows in set
Hint 1

MAX(salary) OVER (PARTITION BY dept_id) - salary.

Quick quiz

1. What is the main difference between GROUP BY and a window function?

2. What does PARTITION BY dept_id do?

3. Can you use a window function in WHERE?

4. Which MySQL version introduced window functions?

Window functions FAQ

What are window functions in SQL?

Functions that compute a value over a set of rows related to the current row, defined with OVER(), without collapsing the rows into groups.

What is the difference between PARTITION BY and GROUP BY?

GROUP BY returns one row per group. PARTITION BY defines groups for a window function while keeping every row in the result.

Does MySQL support window functions?

Yes, from MySQL 8.0, including ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE and aggregates with OVER.

Why can’t I filter on a window function in WHERE?

Because window functions are evaluated after WHERE. Compute them in a CTE or subquery and filter in the outer query.