Step 1 Solve it yourself first
Tables you will use
What it really tests
Comparing a with an of its own group — three valid ways (, to a grouped , ) and knowing when each fits.
The pattern: Compare a row with its group
"Above the average for their…", "more than their team's…", "compared with others in the same…".
Picture it
flowchart LR
E["each employee"] --> A["AVG(salary) OVER<br/>(PARTITION BY dept_id)"]
A --> C{"salary above<br/>dept average?"}
C -->|"yes"| K["Asha, Hana, Dara"]The approach, step by step
- Work out each department's average salary.
- Put that average next to each employee — a window function does both in one step.
- Keep rows where salary > the department average, filtering outside the .
- Decide what to do with Gus, who has no department.
Your turn: write it before you look
Your queries in this box
Show the solution
Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.
Solution: a window average, filtered outside
WITH with_avg AS (
SELECT name, dept_id, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg
FROM employees
)
SELECT name, dept_id, salary, dept_avg
FROM with_avg
WHERE salary > dept_avg
ORDER BY dept_id, salary DESC;3 rows
| name | dept_id | salary | dept_avg |
|---|---|---|---|
| Asha | 1 | 95000 | 78750 |
| Hana | 1 | 80000 | 78750 |
| Dara | 2 | 60000 | 52500 |
Fatima is alone in Marketing, so she equals her own average. Gus forms a group of one and also equals his average.
Another way: Join to a grouped subquery
SELECT e.name, e.dept_id, e.salary, a.dept_avg
FROM employees e
JOIN (
SELECT dept_id, AVG(salary) AS dept_avg
FROM employees
GROUP BY dept_id
) AS a ON a.dept_id = e.dept_id
WHERE e.salary > a.dept_avg
ORDER BY e.dept_id, e.salary DESC;3 rows
| name | dept_id | salary | dept_avg |
|---|---|---|---|
| Asha | 1 | 95000 | 78750 |
| Hana | 1 | 80000 | 78750 |
| Dara | 2 | 60000 | 52500 |
Works in any , even without window functions. Gus drops out here because NULL = NULL is not true in the join.
Edge cases to say out loud
- An employee with no department: the window version groups NULLs together; the join version drops them. Ask which is wanted.
- A department of one person can never be above its own average.
- "Above" means strictly greater; say so.
Common mistakes
WHERE salary > AVG(salary)— an aggregate in is not allowed, and it would be the company average anyway.- Comparing with the company average when the question says "their department".
- Filtering on the window function in the same 's WHERE.
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.
-
Read
PostgreSQL docs — Window Functions (tutorial)
official documentation, free
-
Practice
DataLemur — window function questions
real interview questions, many 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 three ways to compare a row with its group's average.
Show answer
1. A window function:
AVG(salary) OVER (PARTITION BY dept_id)puts the department average on every row; a CTE then filterssalary > dept_avg. One pass, and the average is right there to show.
2. Join to a grouped subquery:SELECT dept_id, AVG(salary) … GROUP BY dept_idmakes one row per department, then you join it to employees on dept_id and compare. Works in every database, even ones without window functions.
3. A correlated subquery:WHERE salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.dept_id = e.dept_id). Reads naturally, but is worked out once per row (good optimisers rewrite it), and the average isn't available to display. -
Writefrom a blank page, then run it
List orders whose amount is above that customer's average order amount.
Show answer
Partition the average by customer instead of department. Priya averages 85 (425 ÷ 5), so orders 101 (120) and 105 (150) are above it; Tom averages 137.5, so 104 (200) is. Lena's average is 60 (the NULL order is skipped), and her only amount equals it, so nothing of hers is above.
sqlWITH with_avg AS ( SELECT order_id, customer_id, amount, AVG(amount) OVER (PARTITION BY customer_id) AS customer_avg FROM orders ) SELECT order_id, customer_id, amount, customer_avg FROM with_avg WHERE amount > customer_avg ORDER BY customer_id, order_id;3 rows
order_id customer_id amount customer_avg 101 1 120 85 105 1 150 85 104 2 200 137.5 -
Predictsay it before you run it
If Chen's salary rose to 90000, who would the solution return?
Show answer
Asha, Chen and Dara. Engineering's average rises to (95,000 + 70,000 + 90,000 + 80,000) ÷ 4 = 83,750. Asha (95,000) and Chen (90,000) are above it; Hana (80,000) drops below. Sales is unchanged, so Dara is still above 52,500.
sqlUPDATE employees SET salary = 90000 WHERE name = 'Chen'; WITH with_avg AS ( SELECT name, dept_id, salary, AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg FROM employees ) SELECT name, dept_id, salary, dept_avg FROM with_avg WHERE salary > dept_avg ORDER BY dept_id, salary DESC;3 rows
name dept_id salary dept_avg Asha 1 95000 83750 Chen 1 90000 83750 Dara 2 60000 52500 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Make the solution exclude employees with no department, and explain why the join version already does.
Show answer
Add
WHERE dept_id IS NOT NULLinside the CTE, so Gus is never given a "department" of his own. (In the window version, puts all NULLs in one group, so Gus is compared with himself; he isn't above his own average, so he doesn't appear today, but only by luck: two employees with no department would be compared with each other.)The join version excludes him already: it joins on
a.dept_id = e.dept_id, andNULL = NULLis not true, so Gus's row finds no partner and the join drops it.sqlWITH with_avg AS ( SELECT name, dept_id, salary, AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg FROM employees WHERE dept_id IS NOT NULL ) SELECT name, dept_id, salary, dept_avg FROM with_avg WHERE salary > dept_avg ORDER BY dept_id, salary DESC;3 rows
name dept_id salary dept_avg Asha 1 95000 78750 Hana 1 80000 78750 Dara 2 60000 52500
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.
I need each employee's department average next to their row, so I compute of salary over a window partitioned by department in a CTE, and keep rows where salary exceeds it. An equivalent approach joins employees to a grouped subquery of averages per department. I'd mention the NULL-department case, since the two approaches treat it differently.
Follow-up questions
How would you show how far above the average each person is, as a percentage?
ROUND(100.0 * (salary - dept_avg) / dept_avg, 1) in the outer .
Which approach is fastest?
Usually the window function, because it reads the once. The often makes them similar; check the plan.
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.