You're going to learn RANK once and then spend your whole career choosing between it and ROW_NUMBER and DENSE_RANK. That's the real skill. The syntax takes ten minutes. Deciding which one to reach for takes a few hundred reps, and nobody walks you through the decision until you've already bombed one interview on it.
We're going to walk it together. Start with one rule: RANK leaves gaps after ties, DENSE_RANK doesn't, and ROW_NUMBER pretends ties don't exist. That single sentence will carry you through 90% of the interview questions you'll see. The other 10% comes down to understanding why the gap matters at all.
Use RANK/DENSE_RANK
Use ROW_NUMBER
Ranking functions to learn
Rule about tie gaps
Source: DataDriven analysis of 1,042 verified data engineering interview rounds.
RANK is a window function. It requires an OVER clause with ORDER BY. PARTITION BY is optional. Without PARTITION BY, the entire result set is treated as one group.
RANK() OVER (
[PARTITION BY partition_column]
ORDER BY sort_column [ASC | DESC]
)Divides the result set into groups. RANK restarts at 1 for each group. Without PARTITION BY, all rows belong to a single group. You can partition by one or more columns. PARTITION BY department creates one ranking per department. PARTITION BY department, fiscal_year creates one ranking per department-year combination.
Defines the ranking criteria. ORDER BY salary DESC ranks the highest salary as 1. ORDER BY created_at ASC ranks the oldest record as 1. You must include ORDER BY; without it, most engines throw an error. SQL Server allows omitting it but returns nondeterministic results, which is never what you want.
-- Basic RANK: global ranking by score
SELECT
student_name,
exam_score,
RANK() OVER (ORDER BY exam_score DESC) AS score_rank
FROM exam_results;
-- RANK with PARTITION BY: ranking per department
SELECT
department,
employee_name,
salary,
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS dept_salary_rank
FROM employees;If you remember nothing else from this page, remember that ties are the entire point of RANK. You'll see ORDER BY salary DESC in almost every interview example, and the moment two rows tie, RANK assigns the same number and jumps ahead. That gap tells you how many rows shared the spot. Five students at score 90 all get rank 2, and the next student gets rank 7. The formula is simple: next rank equals current rank plus number of tied rows.
Tie behavior with RANK()
| Name | Score | RANK() |
|---|---|---|
| Alice | 95 | 1 |
| Bob | 90 | 2 |
| Carol | 90 | 2 |
| Dave | 85 | 4 |
| Eve | 80 | 5 |
Key insight: Bob and Carol both score 90 and both get rank 2. Dave gets rank 4, not 3. Rank 3 does not exist because positions 2 and 3 were both consumed by the tie. This is the gap that interviewers ask about.
SELECT
student_name,
score,
RANK() OVER (ORDER BY score DESC) AS rnk
FROM exam_results;
-- Alice 95 1
-- Bob 90 2
-- Carol 90 2
-- Dave 85 4 <-- rank 3 is skipped
-- Eve 80 5PARTITION BY creates independent ranking windows. Each partition starts its ranking at 1. This is how you answer "rank employees within each department" or "top N per group" questions. Without PARTITION BY, every row competes against every other row globally. With it, rows only compete against rows in the same partition.
RANK() PARTITION BY department ORDER BY salary DESC
| Department | Name | Salary | RANK() |
|---|---|---|---|
| Engineering | Alice | $150,000 | 1 |
| Engineering | Bob | $140,000 | 2 |
| Engineering | Carol | $140,000 | 2 |
| Engineering | Dave | $130,000 | 4 |
| Marketing | Eve | $120,000 | 1 |
| Marketing | Frank | $115,000 | 2 |
| Marketing | Grace | $110,000 | 3 |
Engineering and Marketing each have their own ranking sequence. Bob and Carol tie at rank 2 within Engineering, so Dave gets rank 4 within Engineering. Marketing starts fresh at rank 1 for Eve. The partitions are completely independent.
SELECT
department,
employee_name,
salary,
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS dept_rank
FROM employees;You can partition by more than one column. PARTITION BY department, fiscal_year creates a separate ranking for each department-year combination. If you have 5 departments and 3 years, you get 15 independent ranking sequences. Each combination restarts at 1.
SELECT
department,
fiscal_year,
employee_name,
sales_total,
RANK() OVER (
PARTITION BY department, fiscal_year
ORDER BY sales_total DESC
) AS yearly_sales_rank
FROM annual_sales;RANK is the right choice when ties should share a position and the gap after ties communicates meaningful information. If you need consecutive numbers, use DENSE_RANK. If you need exactly one row per group, use ROW_NUMBER.
When two contestants tie for 2nd place, there should be no 3rd place. The next contestant gets 4th. This is exactly what RANK does. The gap communicates that positions were consumed by the tie. Sports leaderboards, sales contests, and any ranking where ties share a position and the gap matters use RANK.
SELECT
salesperson_id,
region,
total_revenue,
RANK() OVER (
PARTITION BY region
ORDER BY total_revenue DESC
) AS revenue_rank
FROM quarterly_sales
WHERE fiscal_quarter = '2026-Q1';RANK is useful when you want to know the exact position of a row relative to all others, including the effect of ties. If 10 people tie for 1st, the 11th person is rank 11, not rank 2. That accurately reflects the competitive position. Use this when you need to answer questions like 'how many people scored higher than this row?'
SELECT
student_id,
exam_score,
RANK() OVER (ORDER BY exam_score DESC) AS score_rank,
COUNT(*) OVER () AS total_students
FROM exam_results;
-- If score_rank = 15 and total_students = 200,
-- exactly 14 students scored higherWhen you write WHERE rank <= 3, RANK might return more than 3 rows if ties exist at the boundary. This is a feature, not a bug. If two people tie for 3rd, both are included. This behavior is what interviewers test. They want to see that you understand the result set size is not guaranteed when using RANK with a filter.
WITH ranked AS (
SELECT
product_id,
category,
units_sold,
RANK() OVER (
PARTITION BY category
ORDER BY units_sold DESC
) AS sales_rank
FROM product_sales
)
SELECT * FROM ranked WHERE sales_rank <= 3;
-- May return 4+ rows per category if ties exist at rank 3All three are ranking window functions. They only differ in how they handle ties. Picking the wrong one is a silent bug: the query runs, returns results, and the interviewer waits to see if you notice.
| Function | Ties? | Gaps? | Best For |
|---|---|---|---|
| RANK() | Yes | Yes | Competition rankings, positional analysis |
| DENSE_RANK() | Yes | No | Nth distinct value, consecutive numbering |
| ROW_NUMBER() | No | No | Deduplication, exactly one row per group |
Interview tip: If the question says "top 3 employees," use ROW_NUMBER (exactly 3 rows). If it says "top 3 salaries," use DENSE_RANK (all employees at the top 3 salary levels). If it says "rank with ties," use RANK.
These questions test whether you understand RANK beyond the textbook definition. Each includes what the interviewer is actually evaluating and how to structure your answer.
What they test:
Conceptual clarity on tie handling. The interviewer wants you to describe the gap behavior. RANK skips numbers after ties; DENSE_RANK does not. With scores 95, 90, 90, 85: RANK gives 1, 2, 2, 4. DENSE_RANK gives 1, 2, 2, 3. The difference matters when filtering: WHERE rank <= 3 with RANK excludes the score of 85 (rank 4), while WHERE dense_rank <= 3 includes it (dense_rank 3).
Approach:
Draw a concrete example with tied values. Show both outputs side by side. Then state when each is appropriate: RANK for positional ranking where gaps matter, DENSE_RANK when you need the Nth distinct value.
What they test:
This is where RANK shines vs ROW_NUMBER. ROW_NUMBER would return exactly 2 rows per department, breaking ties arbitrarily. RANK returns all employees tied at rank 1 or 2. The interviewer specifically said 'including ties' to signal that RANK (or DENSE_RANK) is the right choice.
Approach:
Use RANK() OVER (PARTITION BY department ORDER BY salary DESC) in a CTE, then filter WHERE rnk <= 2. Mention that the result may contain more than 2 rows per department if ties exist, and that this is the intended behavior.
What they test:
Real-world debugging of RANK behavior. The gaps after ties (1, 2, 2, 4) look like missing data to business users who expect consecutive numbers. The fix depends on the requirement: switch to DENSE_RANK if consecutive numbers are needed, or add a tooltip explaining the gap convention.
Approach:
Explain that RANK intentionally skips numbers after ties. If the business needs consecutive numbers, use DENSE_RANK. If the business needs exactly one row per rank, use ROW_NUMBER. Clarify the requirement before changing the query.
NULLs in the ORDER BY column affect rank assignments. The position of NULLs depends on the engine. PostgreSQL places NULLs last for ASC and first for DESC by default. SQL Server places NULLs first for ASC. Multiple NULL rows are treated as ties and receive the same rank.
-- Control NULL placement explicitly
SELECT
employee_name,
salary,
RANK() OVER (
ORDER BY salary DESC NULLS LAST
) AS salary_rank
FROM employees;
-- NULLs are ranked together at the bottom
-- All NULL salary rows share the same rankBest practice: Always use NULLS LAST or NULLS FIRST explicitly when your ORDER BY column might contain NULLs. In engines that do not support this syntax (like older MySQL versions), use COALESCE to replace NULLs with a sentinel value.
The tie-gap rule sticks after ten or twelve practice problems. We'll walk you through the easy ones first and save the tricky ones for when you're ready.