SQL RANK() OVER (PARTITION BY) Explained with Examples
PARTITION BY is the primitive underneath every cohort dashboard, every leaderboard, every top-N-per-group report you have ever rendered. It appears in 21% of verified DE interview SQL rounds, and almost all of those pair it with a ranking function. The pattern is so common that most senior interview rubrics call it out as a pass/fail signal for the 'can you read a fact table' bar.
Syntax Breakdown
Every part of RANK() OVER (PARTITION BY ... ORDER BY ...) has a specific job. Understanding each component is the difference between copying a pattern and knowing when to reach for it.
Examples
The four examples below trace the same pipeline stage at increasing levels of sophistication: a basic rank per partition, a top-N filter sitting downstream of the rank, a deduplication pattern on a natural key, and the aggregate-then-rank shape that powers most cohort dashboards. Treat each one as a reusable component in your query architecture.
Know Window Functions the way the interviewer who asks it knows it.
Top Selling Items
Revenue crowns the winners. Who sold the most?
Pulled from debriefs where SQL was the gate.
Syntax Breakdown
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS salary_rankSyntax Parts
RANK()
The ranking function. It assigns a rank to each row within its partition based on the ORDER BY clause. If two rows have the same value, they get the same rank, and the next rank is skipped. For example: 1, 2, 2, 4 (rank 3 is skipped because two rows tied at rank 2).
OVER(...)
The window specification. This tells SQL how to define the 'window' of rows for the ranking. Without OVER, RANK() would not know what to rank or how to partition the data.
PARTITION BY column
Divides the result set into independent groups. RANK() restarts at 1 for each partition. Think of it as GROUP BY for window functions, except it does not collapse rows. Every row keeps its identity.
ORDER BY column
Determines the ranking order within each partition. ORDER BY salary DESC means the highest salary gets rank 1. Without ORDER BY, the rank would be meaningless because there is no ordering to rank by.
Basic: Rank employees by salary within each department
SELECT
department,
name,
salary,
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS salary_rank
FROM employees;
-- department | name | salary | salary_rank
-- ------------|---------|---------|------------
-- Engineering | Alice | 180000 | 1
-- Engineering | Bob | 165000 | 2
-- Engineering | Carol | 165000 | 2
-- Engineering | Dave | 150000 | 4
-- Marketing | Eve | 140000 | 1
-- Marketing | Frank | 130000 | 2Bob and Carol both earn $165K, so they both get rank 2. Dave gets rank 4, not rank 3. The gap in ranks tells you that ties exist above.
Top-N per group: Find the top 3 earners in each department
WITH ranked AS (
SELECT
department,
name,
salary,
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS rn
FROM employees
)
SELECT department, name, salary
FROM ranked
WHERE rn <= 3;If there are ties at rank 3, RANK() returns all tied rows. That means you might get more than 3 rows per department. If you want exactly 3, use ROW_NUMBER() instead. Interviewers specifically test whether you understand this distinction.
Rank customers by total spend
WITH customer_spend AS (
SELECT
customer_id,
region,
SUM(amount) AS total_spend
FROM orders
GROUP BY customer_id, region
)
SELECT
region,
customer_id,
total_spend,
RANK() OVER (
PARTITION BY region
ORDER BY total_spend DESC
) AS spend_rank
FROM customer_spend;The CTE computes total_spend with GROUP BY. The outer query applies RANK() to the aggregated result. This two-step pattern (aggregate, then rank) is extremely common.
Find the most recent order per customer
WITH latest AS (
SELECT
customer_id,
order_id,
order_date,
amount,
RANK() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC
) AS rn
FROM orders
)
SELECT customer_id, order_id, order_date, amount
FROM latest
WHERE rn = 1;If a customer has two orders on the same date, RANK() returns both. Use ROW_NUMBER() if you want exactly one row per customer regardless of ties.
RANK vs ROW_NUMBER vs DENSE_RANK
The three ranking functions differ only in how they handle ties. Knowing which one to use is a common interview differentiator.
ROW_NUMBER()
Always assigns unique sequential numbers. No ties. 1, 2, 3, 4, 5. Best for: Deduplication, pagination, exactly-N-per-group queries. When you need exactly one row or exactly N rows, no exceptions. Example: 1, 2, 3, 4, 5 (even if rows 2 and 3 have the same value).
RANK()
Assigns the same rank to tied rows. Skips subsequent ranks. 1, 2, 2, 4. Best for: Competition-style ranking where ties should be visible and the gap communicates how many items tied. Sports rankings, leaderboards. Example: 1, 2, 2, 4 (rank 3 is skipped).
DENSE_RANK()
Assigns the same rank to tied rows. Does NOT skip subsequent ranks. 1, 2, 2, 3. Best for: When you need consecutive rank numbers without gaps. Useful for bucketing or when the rank number itself is used in downstream logic. Example: 1, 2, 2, 3 (no gaps).
3 RANK OVER PARTITION Interview Questions
Three questions that test RANK OVER PARTITION understanding. Each includes the hint and recommended approach.
For each department, find the employee with the second-highest salary. If there is a tie for the highest salary, the second-highest is the next distinct salary below the tie.
DENSE_RANK is the right choice here, not RANK. With RANK, if two employees tie for first place, rank 2 is skipped. With DENSE_RANK, the next distinct salary gets rank 2. Use DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr, then filter WHERE dr = 2.
Write a query that returns the top 2 products by revenue in each category. If there are ties, include all tied products.
RANK() handles this naturally because it assigns the same rank to tied rows. Filter WHERE rank <= 2 and you get all products at rank 1 and rank 2, including ties. Use RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn in a CTE, then WHERE rn <= 2.
For each customer, flag their orders as 'first', 'second', or 'other' based on order date. If two orders share the same date, both should be flagged with the same label.
Use DENSE_RANK for consecutive numbering, then CASE WHEN to map ranks to labels. PARTITION BY customer, ORDER BY date. CASE WHEN DENSE_RANK() OVER (PARTITION BY customer_id ORDER BY order_date) = 1 THEN 'first' WHEN ... = 2 THEN 'second' ELSE 'other' END AS order_label.
RANK OVER PARTITION FAQ
Can I use RANK() without PARTITION BY?+
Why can I not use RANK() in a WHERE clause directly?+
When should I use RANK() vs ROW_NUMBER() in an interview?+
Build it once, reuse it everywhere
- 01
Active recall beats re-reading by 50%
Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom
- 02
76% of hiring managers reject on the coding task, not the resume
From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice
- 03
Five problem shapes cover 80% of data engineer loops
Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition