Window Functions: RANK() and PARTITION BY
Rank employees by salary within their own department using RANK() OVER (PARTITION BY ...), without collapsing rows the way GROUP BY does.
Overview
Window functions compute a value across a set of related rows — a
"window" — without collapsing those rows into a single output row the way
GROUP BY does. RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) gives every employee a rank relative to their own department's
salaries, while still returning one row per employee.
This is the tool for "top N per group" reports, leaderboards, and any
situation where you need both the individual row and its position within
a group — something a plain GROUP BY can't give you, since it discards
the individual rows.
The Query
Loading database engine...
Step-by-Step Breakdown
RANK() OVER (...)— theRANK()function assigns a rank number to each row based on the ordering defined insideOVER (...).PARTITION BY department_id— resets the ranking for each department independently. Without this,RANK()would rank every employee against the entire company at once.ORDER BY salary DESC(insideOVER) — defines what "rank" means here: the highest-paid employee in each department gets rank 1.- The outer
SELECT— every original employee row is still present;salary_rankis just an extra computed column alongside it. UnlikeGROUP BY, no rows are lost or merged. ORDER BY department_id, salary_rank(outside) — this is a separate, ordinaryORDER BYon the final result set, controlling display order rather than the ranking logic itself.
RANK() vs. DENSE_RANK() vs. ROW_NUMBER()
RANK() leaves gaps after ties (two employees tied for 1st means the next
rank is 3, not 2). DENSE_RANK() doesn't leave gaps. ROW_NUMBER() never
ties at all — it assigns a unique sequential number even when the
underlying values are equal. Pick based on whether ties and gaps matter for
your use case.
Variations
Compare each employee's salary to their department's average, side by side:
Loading database engine...
Here there's no ORDER BY inside the OVER (...) clause at all, because
AVG over a partition doesn't depend on row order — every employee in a
department sees the same average, computed across their whole department's
partition.
Common Mistakes
- Confusing window functions with
GROUP BY. A window function keeps every input row;GROUP BYcollapses rows into one per group. Reach for a window function specifically when you need the detail rows and an aggregate context alongside them. - Forgetting
PARTITION BYentirely. Without it, the window covers the whole result set, soRANK()ranks every employee company-wide instead of within their department — a subtle bug that only shows up once you check the numbers. - Putting a window function in the
WHEREclause. Window functions are computed afterWHEREfiltering, so you can't referencesalary_rankthere directly — you'd need to wrap the query in a CTE or subquery and filter on the outside. - Assuming
RANK()always starts at 1 for the whole table. It restarts at 1 for every partition, so rank 1 appears once per department here, not once overall.