In business analytics and data engineering, ranking items is one of the most common tasks: finding the top 3 salespeople in every branch, surfacing the best-selling products per category, or identifying repeat customers with the highest lifetime value. Understanding how SQL RANK works, and how it differs from DENSE_RANK and ROW_NUMBER, is a staple of technical SQL interviews.
In this tutorial, connecting to core concepts in what is SQL and our SQL window functions guide, we analyze the SQL RANK function, examine concrete side-by-side examples with tied values, and demonstrate how to write clean Top-N queries using CTEs.
What is the SQL RANK Function?
The SQL RANK function evaluates rows across a specified "window" of data without collapsing individual records into aggregate rows:
sql
SELECT employee_name, department, salary, RANK() OVER (ORDER BY salary DESC) AS global_salary_rankFROM employees;
Syntax breakdown:
RANK(): The ranking function. It takes no arguments.
OVER (...): Defines the window frame.
PARTITION BY (optional): Divides records into independent subsets (like departments or regions).
ORDER BY: Dictates the sorting criteria on which the ranking sequence is determined.
SQL RANK vs DENSE_RANK vs ROW_NUMBER
To see the exact differences, observe how all three window functions evaluate a student examination score table with duplicate scores:
sql
-- Sample Data:-- student_name | score-- Alice | 98-- Bob | 92-- Charlie | 92-- David | 85-- Eve | 80SELECT student_name, score, ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num, RANK() OVER (ORDER BY score DESC) AS rank_val, DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank_valFROM exam_scores;
Output Evaluation:
Feature / Criteria
Quick Rule of Thumb
Use ROW_NUMBER() when you need unique sequential identifiers (e.g., deduplicating rows or pagination).
Use RANK() when you want true athletic podium ranking (e.g., two silver medalists means no bronze awarded).
Use DENSE_RANK() when you want continuous tier groupings (e.g., top 3 pricing levels with no skipped numbers).
Advanced SQL RANK Examples: PARTITION BY and Top-N Filtering
1. Partitioning by Group (e.g., Department or Country)
When you include PARTITION BY, the SQL RANK sequence resets to 1 for every distinct group:
sql
SELECT employee_name, department, salary, RANK() OVER ( PARTITION BY department ORDER BY salary DESC ) AS dept_salary_rankFROM employees;
Each department receives its own independent rank 1, 2, 3...
2. The Top-N Query Pattern Using CTEs
A frequent interview trap asks candidates to retrieve the "top 2 earners per department". If you write:
sql
-- INVALID QUERY: Syntax Error!SELECT employee_name, department, salaryFROM employeesWHERE RANK() OVER (PARTITION BY department ORDER BY salary DESC) <= 2;
This query fails immediately because window functions are calculated after the WHERE clause runs. To filter by SQL RANK, wrap the window calculation in a Common Table Expression (CTE):
sql
-- VALID & PERFORMANT TOP-N PATTERNWITH ranked_employees AS ( SELECT employee_name, department, salary, DENSE_RANK() OVER ( PARTITION BY department ORDER BY salary DESC ) AS salary_rank FROM employees)SELECT employee_name, department, salary, salary_rankFROM ranked_employeesWHERE salary_rank <= 2ORDER BY department, salary_rank;
The true power of the sql rank function emerges when combined with Common Table Expressions (CTEs), multi-level partitioning, and moving window frames to solve complex business questions. Explore more guides in our SQL Tutorials hub.
Finding Top-3 Performers per Category with CTE Filtering
A frequent business request is finding the top 3 selling products within each regional category:
sql
WITH RankedProducts AS ( SELECT category_name, product_name, total_sales, RANK() OVER ( PARTITION BY category_name ORDER BY total_sales DESC ) AS sales_rank FROM product_category_sales)SELECT category_name, sales_rank, product_name, total_salesFROM RankedProductsWHERE sales_rank <= 3ORDER BY category_name, sales_rank;
Because window functions cannot be evaluated directly inside a WHERE clause (due to SQL's order of execution), wrapping the query in a CTE computes the rankings first, enabling downstream filtering on sales_rank <= 3.
Dense Ranking vs Percentage Ranking (PERCENT_RANK and CUME_DIST)
In addition to discrete integer ranking, SQL provides statistical cumulative distribution functions:
PERCENT_RANK(): Evaluates the relative percentile rank of a row between 0.0 and 1.0:
PERCENT_RANK = (RANK - 1) / (Total Rows - 1)
CUME_DIST(): Measures cumulative distribution (the fraction of rows with values less than or equal to the current row's value).
NTILE(buckets): Splits ranked rows into equal quantiles (e.g., NTILE(4) divides customers into quartiles; NTILE(10) creates deciles for credit scoring).
sql
SELECT customer_id, lifetime_spend, RANK() OVER (ORDER BY lifetime_spend DESC) AS raw_rank, DENSE_RANK() OVER (ORDER BY lifetime_spend DESC) AS dense_rnk, NTILE(4) OVER (ORDER BY lifetime_spend DESC) AS spend_quartile, ROUND(PERCENT_RANK() OVER (ORDER BY lifetime_spend DESC)::NUMERIC, 3) AS percentileFROM customer_ltv;
Real-World Business Scenario: Customer Re-Engagement RFM Scoring
In customer relationship management (CRM) analytics, ranking functions are combined with quantiles to identify VIP customers and churn risks:
sql
WITH customer_aggregates AS ( SELECT customer_id, CURRENT_DATE - MAX(order_date) AS days_since_last_order, COUNT(order_id) AS lifetime_orders, SUM(order_total) AS total_monetary_spend FROM orders GROUP BY customer_id)SELECT customer_id, days_since_last_order, RANK() OVER (ORDER BY total_monetary_spend DESC) AS spend_rank, NTILE(5) OVER (ORDER BY total_monetary_spend DESC) AS monetary_quintile, NTILE(5) OVER (ORDER BY days_since_last_order ASC) AS recency_quintileFROM customer_aggregates;
Handling NULL Values in SQL RANK
In SQL sorting, NULL values can disrupt your ranking order if not explicitly controlled:
sql
-- In PostgreSQL/Oracle, NULLs sort FIRST by default in DESC ordering.-- Use NULLS LAST to force NULL values to the bottom:SELECT sales_rep_name, quota_attainment_pct, RANK() OVER ( ORDER BY quota_attainment_pct DESC NULLS LAST ) AS attainment_rankFROM sales_performance;
Check out our complete overview of SQL for Data Analyst for advanced real-world analytics workflows.
Summary Checklist for SQL RANK
Use RANK() when tied rows should skip subsequent rank values.
Use DENSE_RANK() when you want continuous ranks without gaps.
Use ROW_NUMBER() when every record requires a distinct integer.
Always wrap ranking queries in a CTE or subquery to filter by rank (WHERE rank <= N).
Explicitly specify NULLS LAST to prevent unassigned values from capturing Rank 1.
SQL RANK in Cohort Retention and Customer Milestones
Beyond competitive leaderboards, senior analysts utilize the sql rank function to map customer lifecycle milestones:
Flagging Milestone Purchases: By ranking transactions per customer chronologically (ORDER BY order_date ASC), analysts isolate customer order 1 (acquisition), order 2 (activation/retention), and order 5 (loyalty tier).
Calculating Days Between Milestone Orders: Joining the ranked order table to itself allows measuring median days between first and second purchase, providing product marketing teams with the exact window for automated re-engagement email drips.
When business rules require deterministic, non-arbitrary rank order even when primary metrics tie:
Multi-Column ORDER BY within OVER():
sql
SELECT student_name, score, submission_time, RANK() OVER ( ORDER BY score DESC, submission_time ASC ) AS tie_broken_rankFROM exam_submissions;
Adding submission_time ASC guarantees that if two students score 98%, the student who submitted earlier receives the higher rank.
Deterministic ROW_NUMBER Fallback: In financial trading leaderboards or queue processing where ties are forbidden, ROW_NUMBER() OVER (ORDER BY score DESC, account_id ASC) ensures every single record receives a unique sequential integer from 1 to $N$.
Checklist for Using SQL RANK in Production Queries
Before deploying analytical ranking queries to production pipelines or business intelligence data models, verify this technical checklist:
Have you chosen the correct ranking variant? Use RANK() if ties should skip ranks (1, 2, 2, 4), DENSE_RANK() if ranks must remain contiguous without gaps (1, 2, 2, 3), and ROW_NUMBER() for unique sequential pagination keys.
Did you include secondary tie-breaker columns in your ORDER BY clause inside OVER() to guarantee deterministic query outputs?
Remember that window functions cannot be filtered in WHERE; always wrap your ranking calculation inside a Common Table Expression (CTE) or derived table before applying threshold conditions like WHERE rank <= 10.
The SQL RANK function is a window function that assigns a sequential ranking number to each row within an ordered result set or partition. When two or more rows have identical values (ties), they receive the same rank, and subsequent ranks are skipped.
What is the difference between RANK and DENSE_RANK in SQL?
Both functions assign identical ranks to tied values. However, RANK skips ranks after ties (e.g., 1, 2, 2, 4), whereas DENSE_RANK does not skip any ranks (e.g., 1, 2, 2, 3).
How does ROW_NUMBER differ from SQL RANK?
ROW_NUMBER assigns a strictly unique, contiguous integer to every row (1, 2, 3, 4...) regardless of whether the values in the ORDER BY clause are tied.
Can you filter by the SQL RANK function directly in a WHERE clause?
No. Because window functions are evaluated in Phase 5 of SQL query processing (after WHERE and GROUP BY), you cannot place RANK() directly in a WHERE clause. You must wrap the query in a Common Table Expression (CTE) or subquery.
How does SQL RANK handle NULL values?
By default in SQL, NULL values are treated as either the highest values (PostgreSQL/Oracle default with NULLS FIRST) or lowest values (MySQL/SQL Server). To guarantee consistent ranking order, explicitly use 'ORDER BY column DESC NULLS LAST'.
Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.