Intermediate: combining and changing data · Window functions · topic 32 of 62 · 35 min
OVER and PARTITION BY
Explanation
The problem GROUP BY cannot solve
In module 4 you used GROUP BY to get one row per department with its average salary. But a manager often wants something else: every employee, with their own salary and the department average next to it, so she can see who is above or below. GROUP BY cannot do that, because it collapses each group into a single row and the individual names are gone.
SELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id;
-- NULL | 45000
-- 1 | 140000
-- 2 | 80666.67 (rounded here)
-- 3 | 86500
-- 4 | 80000A window function keeps the rows
A window function works out a value over a set of rows related to the current row, called the window, and writes the answer on the current row. No row disappears. You turn an ordinary aggregate such as SUM, AVG, COUNT, MIN or MAX into a window function by adding the word OVER after it.
SELECT name, dept_id, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg
FROM employees;
-- Meera Iyer | 1 | 250000 | 140000
-- Rahul Sharma | 1 | 120000 | 140000
-- Anita Desai | 1 | 95000 | 140000
-- Vikram Singh | 1 | 95000 | 140000
-- Pooja Gupta | 2 | 110000 | 80666.666...
-- ... 11 rows in allOVER () and PARTITION BY
- OVER () with empty brackets: the window is every row of the result. SUM(salary) OVER () puts the total payroll, 1100000, on all 11 rows.
- OVER (PARTITION BY dept_id): the rows are split into partitions, one per department, and each row sees only its own partition. A partition is like a group in GROUP BY, except the rows are not squeezed together.
- You can partition by several columns: PARTITION BY city, course.
- NULL forms its own partition, just as it forms its own group in GROUP BY. Deepak (no department) is alone in his partition, so his "department average" is his own salary.
Comparing a row with its group
Because the group value sits on the same row, you can do arithmetic with it. Two everyday questions are "how far is each person from the department average?" and "what share of the department payroll does each person take?".
SELECT name, dept_id, salary,
salary - AVG(salary) OVER (PARTITION BY dept_id) AS vs_avg,
ROUND(100.0 * salary / SUM(salary) OVER (PARTITION BY dept_id), 1) AS pct_of_dept
FROM employees
WHERE dept_id = 1;
-- Meera Iyer | 1 | 250000 | 110000 | 44.6
-- Rahul Sharma | 1 | 120000 | -20000 | 21.4
-- Anita Desai | 1 | 95000 | -45000 | 17
-- Vikram Singh | 1 | 95000 | -45000 | 17When is the window worked out?
Window functions run late: after FROM, WHERE, GROUP BY and HAVING, at the same time as the SELECT list, and before ORDER BY and LIMIT. Two things follow. First, a WHERE changes what the window sees: in the query above, the percentages are shares of Engineering only, because the other rows were already filtered out. Second, you cannot use a window function inside WHERE; topic 5 shows the way around that.
You can even put a window over a GROUP BY result. SUM(SUM(qty * price)) OVER () means "the total of the group totals", which gives each category's share of all revenue in one query.
SELECT p.category, SUM(i.qty * i.price) AS revenue,
ROUND(100.0 * SUM(i.qty * i.price) / SUM(SUM(i.qty * i.price)) OVER (), 1) AS pct
FROM order_items i JOIN products p ON p.id = i.product_id
GROUP BY p.category
ORDER BY revenue DESC;
-- Fashion | 7695 | 47.1
-- Electronics | 3998 | 24.5
-- Grocery | 2750 | 16.8
-- Kitchen | 1900 | 11.6Where it works
Window functions are part of standard SQL. They work in PostgreSQL, SQL Server, Oracle, SQLite 3.25 and later, MySQL 8.0 and later, and MariaDB 10.2 and later. Very old MySQL 5.7 servers do not have them.
Common mistakes
- Expecting fewer rows. A window function never removes rows; if you want one row per group, use GROUP BY.
- Writing 100 * salary / SUM(...) and getting whole numbers. Use 100.0 so the division keeps decimals.
- Forgetting that WHERE runs first. If you filter to one department, OVER () totals only that department.
- Mixing a plain column and an aggregate without OVER: SELECT name, AVG(salary) FROM employees returns one row in SQLite and an error in most other databases. Add OVER () or a GROUP BY.
- Putting a window function in WHERE. It gives "misuse of window function". Compute it in a CTE first (topic 5).
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
OVER and PARTITION BY · 1/7
Welcome to window functions, one of the most useful tools in SQL for reports and interviews.
Prefer a video by a teacher? Find free videos on “sql window functions over partition by explained” on YouTube ↗
In real life: A salary benchmarking report in HR
An HR team preparing appraisals usually wants a sheet with one line per employee: salary, the average and the highest salary in the same department, and the gap. A window query produces that sheet straight from the employees table, so nobody has to copy averages by hand from one report into another, and the numbers update when salaries change.
SELECT name, dept_id, salary,
ROUND(AVG(salary) OVER (PARTITION BY dept_id), 2) AS dept_avg,
MAX(salary) OVER (PARTITION BY dept_id) AS dept_max,
MAX(salary) OVER (PARTITION BY dept_id) - salary AS gap_to_top
FROM employees
ORDER BY dept_id, salary DESC;Practice (0/4 done)
Put the department average next to every employee. From employees (COMPANY), return name, dept_id, salary and the average salary of the employee's department rounded to 2 decimal places, named dept_avg. Return all 11 employees.
What share of the department payroll does each Engineering employee (dept_id = 1) take? Return name, salary and 100.0 * salary divided by the department's total salary, rounded to 1 decimal place, named pct_of_dept.
From students, return name, city and the number of students in the same city, named city_count, for every student.
Return name, salary, the total payroll of the whole company named payroll, and each salary as a percentage of that total rounded to 1 decimal place, named pct_of_total. Return all employees.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Ranking: ROW_NUMBER, RANK, DENSE_RANK and NTILEWindow functions · same module
- Running totals, moving averages and framesWindow functions · same module
- LAG, LEAD, FIRST_VALUE and LAST_VALUEWindow functions · same module
- Counting and totals: aggregate functionsBackend Development · Databases and SQL basics
- Correlated subqueries and derived tablesSubqueries, CTEs and set operations
- Classic interview queries, part 1Interview questions and practice projects
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
employees has 11 rows. How many rows does SELECT name, AVG(salary) OVER (PARTITION BY dept_id) FROM employees return?
Answer: 11
A window function never removes rows; each of the 11 rows gets its department average.
What does SUM(salary) OVER () put on each row?
Answer: The total of all salaries in the result
Empty brackets make the window the whole result, so every row shows the same grand total.
A query has WHERE dept_id = 2 and SUM(salary) OVER (). What does the sum cover?
Answer: Only the Sales rows that passed the WHERE
Window functions run after WHERE, so they only see the filtered rows.
What is the main difference between GROUP BY dept_id and OVER (PARTITION BY dept_id)?
Answer: GROUP BY collapses each group into one row; PARTITION BY keeps every row
Both split rows into groups, but only GROUP BY squeezes them together.
Why write 100.0 * salary / SUM(salary) OVER () instead of 100 * salary / SUM(salary) OVER () in SQLite?
Answer: 100.0 makes the division keep decimals instead of dropping them
Integer divided by integer drops the fraction in SQLite and PostgreSQL.
Two employees have dept_id NULL. After GROUP BY dept_id, where are they?Related
In SQL Server, an INTEGER column holds 1 and 2. What does AVG return?Related
Answer: 1
SQL Server returns an integer average for integer input. Use AVG(col * 1.0) there.
From Aggregate functions
What does SELECT city, SUM(amount) FROM orders GROUP BY city return?Related
Answer: One row per city with its total
GROUP BY city makes one group per city, and SUM runs on each group.
Inside WHERE salary > (SELECT AVG(salary) FROM employees WHERE dept_id = dept_id), what does dept_id = dept_id compare?Related
Answer: The inner row with itself, so it is always true (except for NULLs)
Unqualified names refer to the nearest table. Use aliases: e2.dept_id = e.dept_id.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Window functions” before its exam: Ranking: ROW_NUMBER, RANK, DENSE_RANK and NTILE, Running totals, moving averages and frames, LAG, LEAD, FIRST_VALUE and LAST_VALUE, Top-N per group and de-duplication.
- Module 7 exam: Window functions · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Changing data, transactions and ACID. 62 topics left in the course before the final exam.