Step 1 Understand it
Why you need it. "Show each employee with their department's average salary next to them." gives you the averages but throws away the employees; joining the averages back is clumsy. A puts the group's number on every in one step.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- Seven employees with a department. We want each one next to their department's average salary.
- PARTITION BY dept_id splits the rows into windows, one per department — like GROUP BY's piles. But nothing is squashed.
- AVG(salary) is worked out over each window, and every row gets its own window's answer. All 7 rows stay.
- Compare GROUP BY: one row per department, and the individual names are gone. A window function keeps the detail AND adds the group's number.
Picture it
flowchart LR
subgraph W1["window: dept 1 · avg 78750"]
A["Asha 95000"]
B["Ben 70000"]
C["Chen 70000"]
H["Hana 80000"]
end
subgraph W2["window: dept 2 · avg 52500"]
D["Dara 60000"]
E["Eli 45000"]
end
W1 --> R["every row kept,<br/>each with its own window's average"]
W2 --> RThe idea
Add (…) after a function and it becomes a window function. OVER () with nothing inside means "the window is every row". PARTITION BY dept_id splits the rows into windows, one per department, like GROUP BY — but the rows stay separate, and each row gets its own window's answer. inside OVER sets an order within each window, which ranking and need. Any (, AVG, , MIN, MAX) can be used this way, plus special window-only functions such as and .
The shape of it
function(...) OVER (
PARTITION BY group_column
ORDER BY sort_column
)Words in lower case are the parts you fill in.
How it works, step by step
- and produce the rows: 7 employees with a department.
- PARTITION BY dept_id makes three windows: department 1 (4 people), 2 (2 people), 3 (1 person).
AVG(salary)is worked out over each window: 78750, 52500, 52000.- Each row gets the answer for its own window, and no rows are removed.
- OVER () with no partition makes one window of all rows:
COUNT(*) OVER ()is 8 on every row.
Analogy
A class photo where each pupil holds a card. GROUP BY would replace each form group with one person holding the group's average. A window function keeps every pupil in the photo, and each one's card shows their own form group's average.
Where the analogy breaks
In the photo the card is written once. A window function is worked out in the step, after WHERE and GROUP BY — so WHERE can't filter on it. To filter on a window result, wrap the in a .
Worked examples
Each employee next to their department average
SELECT name, dept_id, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg
FROM employees
WHERE dept_id IS NOT NULL
ORDER BY dept_id, name;7 rows
| name | dept_id | salary | dept_avg |
|---|---|---|---|
| Asha | 1 | 95000 | 78750 |
| Ben | 1 | 70000 | 78750 |
| Chen | 1 | 70000 | 78750 |
| Hana | 1 | 80000 | 78750 |
| Dara | 2 | 60000 | 52500 |
| Eli | 2 | 45000 | 52500 |
| Fatima | 3 | 52000 | 52000 |
GROUP BY dept_id would give 3 rows. The window keeps all 7.
Group sizes and the grand total on every row
SELECT name, dept_id,
COUNT(*) OVER (PARTITION BY dept_id) AS dept_size,
COUNT(*) OVER () AS company_size
FROM employees
ORDER BY emp_id;8 rows
| name | dept_id | dept_size | company_size |
|---|---|---|---|
| Asha | 1 | 4 | 8 |
| Ben | 1 | 4 | 8 |
| Chen | 1 | 4 | 8 |
| Dara | 2 | 2 | 8 |
| Eli | 2 | 2 | 8 |
| Fatima | 3 | 1 | 8 |
| Gus | NULL | 1 | 8 |
| Hana | 1 | 4 | 8 |
OVER () is one window holding every row. Gus forms his own partition.
How far each salary is from the department average
SELECT name, salary,
salary - AVG(salary) OVER (PARTITION BY dept_id) AS diff_from_avg
FROM employees
WHERE dept_id = 1
ORDER BY salary DESC, name;4 rows
| name | salary | diff_from_avg |
|---|---|---|
| Asha | 95000 | 16250 |
| Hana | 80000 | 1250 |
| Ben | 70000 | -8750 |
| Chen | 70000 | -8750 |
Must-know points
- A window function keeps every row; GROUP BY collapses rows.
- PARTITION BY = which rows share a window. No PARTITION BY = one window of all rows.
- ORDER BY inside OVER = the order within each window (needed for ranks, LAG and running totals).
- Window functions run in the SELECT step: you can't use them in WHERE, GROUP BY or . Wrap in a CTE to filter.
- Any aggregate becomes a window function when you add OVER.
Common mistakes
- Writing
WHERE ROW_NUMBER() OVER (…) = 1— not allowed. Filter in an outer query. - Adding GROUP BY when you wanted every row kept.
- Confusing the ORDER BY inside OVER (order within the window) with the query's ORDER BY (order of the output).
In other databases
- Window functions work in , , Oracle, 8+ and 3.25+. Older MySQL (5.7) has none.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Watch
SQL window functions explained (video)
YouTube search, free
-
Read
PostgreSQL docs — Window Functions (tutorial)
official documentation, free
-
Read
Mode SQL Tutorial — SQL Window Functions
Mode, free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain the difference between
AVG(salary) … GROUP BY dept_idandAVG(salary) OVER (PARTITION BY dept_id).Show answer
AVG(salary) … GROUP BY dept_idsquashes each department into one row: 8 employees in, 4 rows out (one per department, Gus's NULL included). The names are gone.AVG(salary) OVER (PARTITION BY dept_id)works out the same averages but keeps all 8 rows. Each employee gets their department's average written next to them, so you can compare: "Ben earns 70,000; his department averages 78,750".Use GROUP BY when you want one row per group, and a window function when you want every row plus a group figure.
sqlSELECT dept_id, AVG(salary) AS dept_avg FROM employees GROUP BY dept_id ORDER BY dept_id;4 rows
dept_id dept_avg NULL 38000 1 78750 2 52500 3 52000 -
Writefrom a blank page, then run it
For every order, show the order_id, amount, and the customer's total spend across all their orders.
Show answer
PARTITION BY customer_id makes one window per customer, and SUM adds up that customer's amounts. Every order keeps its own row. Order 106's NULL is skipped, so Lena's total is 60.
sqlSELECT order_id, customer_id, amount, SUM(amount) OVER (PARTITION BY customer_id) AS customer_total FROM orders ORDER BY order_id;10 rows
order_id customer_id amount customer_total 101 1 120 425 102 1 80 425 103 1 45 425 104 2 200 275 105 1 150 425 106 3 NULL 60 107 3 60 60 108 2 75 275 109 1 30 425 110 5 33 33 -
Predictsay it before you run it
What does
SELECT name, MAX(salary) OVER () FROM employees;return? How many rows?Show answer
8 rows, one per employee, each with 95,000 next to the name.
OVER ()is an empty window: no PARTITION BY, so the window is the whole , and the highest salary in the whole table is Asha's 95,000. A window function never removes rows. (The query has no ORDER BY, so add one if you want the names in a fixed order.)sqlSELECT name, MAX(salary) OVER () FROM employees ORDER BY emp_id;8 rows
name MAX(salary) OVER () Asha 95000 Ben 95000 Chen 95000 Dara 95000 Eli 95000 Fatima 95000 Gus 95000 Hana 95000 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Gus has a NULL dept_id. What dept_avg does he get in the first example if you remove the WHERE?
Show answer
38,000: his own salary.
PARTITION BY, like GROUP BY, puts all NULLs into one group. Gus is the only employee with a NULL dept_id, so he is alone in his window and the average of one salary is that salary.
That is technically right but can mislead: "no department" is not a real department. That is why the example filtered him out with
WHERE dept_id IS NOT NULL.sqlSELECT name, dept_id, salary, AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg FROM employees ORDER BY dept_id, name;8 rows
name dept_id salary dept_avg Gus NULL 38000 38000 Asha 1 95000 78750 Ben 1 70000 78750 Chen 1 70000 78750 Hana 1 80000 78750 Dara 2 60000 52500 Eli 2 45000 52500 Fatima 3 52000 52000
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
A window function computes a value over a set of rows related to the current row, defined by the OVER clause — PARTITION BY says which rows share the window and ORDER BY orders them — but unlike GROUP BY it doesn't collapse rows, so each row keeps its detail and gains the computed value. Because they are evaluated in the SELECT phase, I filter on them from an outer query or CTE.
Follow-up questions
GROUP BY or a window function — how do you choose?
If you want one row per group, GROUP BY. If you want every row plus a group value next to it (an average, a rank, a running total), a window function.
Why can't you use a window function in WHERE?
WHERE runs before the SELECT step where window functions are computed. Put the window function in a CTE and filter outside.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.