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.

Last updated: Proudly published by: Jeff Wahl
21%
Use PARTITION BY
~all
Pair with ranking
11%
Use RANK directly
L5+
Expected at senior

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.

Syntax Breakdown

RANK() OVER (
  PARTITION BY department
  ORDER BY salary DESC
) AS salary_rank

Syntax Parts

RANK()

The ranking function. It assigns a rank to each row within its partition based on the ORDER BY clause. If 2 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 2 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.

Examples

The 4 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.

Basic: Rank employees by salary within each department

SELECT
  department,
  emp_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  | 2

Bob 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,
    emp_name,
    salary,
    RANK() OVER (
      PARTITION BY department
      ORDER BY salary DESC
    ) AS rn
  FROM employees
)
SELECT department, emp_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 check whether you understand this distinction.

Rank customers by total spend

WITH customer_spend AS (
  SELECT
    user_id,
    product_id,
    SUM(total_amount) AS total_spend
  FROM transactions
  GROUP BY user_id, product_id
)
SELECT
  product_id,
  user_id,
  total_spend,
  RANK() OVER (
    PARTITION BY product_id
    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 2-step pattern (aggregate, then rank) is extremely common.

Find the most recent order per customer

WITH latest AS (
  SELECT
    user_id,
    transaction_id,
    transaction_date,
    total_amount,
    RANK() OVER (
      PARTITION BY user_id
      ORDER BY transaction_date DESC
    ) AS rn
  FROM transactions
)
SELECT user_id, transaction_id, transaction_date, total_amount
FROM latest
WHERE rn = 1;

If a customer has 2 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 3 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

3 questions that probe RANK OVER PARTITION understanding. Each includes the hint and recommended approach.

Q1

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 2 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.

Q2

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.

Q3

For each customer, flag their orders as 'first', 'second', or 'other' based on order date. If 2 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, DENSE_RANK and ROW_NUMBER differ only when there are ties, and interviewers construct ties deliberately. Working through the SQL interview questions cover, and the SQL practice problems runs them under the follow-up pressure a screen actually applies.

Prepare for the interview
01 / Open invite
02min.

Know Window Functions the way the interviewer who asks it knows it.

a Window Functions query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1SELECT user_id,
2 COUNT(*) AS sessions
3FROM events
4WHERE ts >= NOW() - INTERVAL '7 day'
5
Execute your solution0.4s avg.
LyftInterview question
Solve a Window Functions problem

Per-group ranking is the single most reused pattern in SQL screens. For where it sits in the round and what tends to follow it, see the SQL round guide.

Average Build Duration by Repo

> Slow builds are blocking deploys and the CI/CD team suspects a few repos are dragging down the pipeline. Show the average build duration in seconds for each repository so the team can identify the worst offenders.

RANK OVER PARTITION FAQ

Can I use RANK() without PARTITION BY?+
Yes. Without PARTITION BY, RANK() treats the entire result set as one partition. It ranks all rows against each other instead of ranking within groups. This is valid but less common in interviews. Most interview questions specifically focus on the PARTITION BY component because it requires understanding grouped ranking.
Why can I not use RANK() in a WHERE clause directly?+
Window functions are evaluated after WHERE, HAVING, and GROUP BY. The rank does not exist yet when the WHERE clause runs. You must compute the rank in a subquery or CTE first, then filter on it in the outer query. This is why the CTE pattern (WITH ranked AS ... SELECT ... WHERE rn <= N) is so common.
When should I use RANK() vs ROW_NUMBER() in an interview?+
Use ROW_NUMBER() when you need exactly N rows per group, with no exceptions for ties. Use RANK() when ties should be preserved and the rank gap should communicate the tie. If the interviewer says 'top 3' and 2 items tie for third place, ask whether they want exactly 3 rows or all items ranked 3 or above. That clarifying question itself demonstrates strong SQL understanding.
02 / Why practice

Build it once, reuse it everywhere

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 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

  3. 03

    5 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

Related Guides