Introduction
Welcome to your fourth lesson in the "Intermediate SQL for Analytics" module. In our last session, you learned how to use Common Table Expressions (CTEs) to structure complex queries, making them more readable and logical. We noted that CTEs are a perfect partner for another powerful SQL feature: window functions.
Today, we'll dive into that feature. You will learn to use window functions like ROW_NUMBER, RANK, and DENSE_RANK to partition and rank data. This is one of the most practical skills in SQL for data analysis. It's the key to answering questions like:
- "Who are the top 5 sales representatives in each region?"
- "What is the most recent order for each customer?"
- "Which products have the highest rating in each category?"
These are exactly the kinds of questions that drive insights in Business Intelligence and dashboard creation.
1. What Are Window Functions?
First, let's understand the core concept. You are already familiar with aggregate functions like SUM() and AVG(), which use a GROUP BY clause to collapse multiple rows into a single summary row.
A window function, on the other hand, performs a calculation across a set of rows—called a "window" or "partition"—but it does not collapse the rows. Instead, it returns a value for each and every row based on the other rows in its window.
The basic syntax for a window function is:
FUNCTION_NAME() OVER ( [PARTITION BY column(s)] [ORDER BY column(s)] )
FUNCTION_NAME(): The window function itself (e.g.,RANK()).OVER (...): The clause that defines the "window" of rows the function will consider.PARTITION BY: (Optional) Divides the rows into logical groups or partitions. The function is applied independently to each partition.ORDER BY: Specifies the order of rows within each partition, which is essential for ranking.
Let's start by looking at the three most common ranking functions.
2. The Three Core Ranking Functions
The ranking functions—ROW_NUMBER(), RANK(), and DENSE_RANK()—all assign a numeric rank to each row within a window. Their only difference is how they handle ties (i.e., when multiple rows have the same value in the ORDER BY column).
SQL RANK vs DENSE_RANK vs ROW_NUMBER
This article from PracticeWindowFunctions.com provides a fantastic side-by-side comparison of the three ranking functions. It gets straight to the point.
Read the sections that provide the summary table and the example result. Focus on the table that defines how each function handles ties, and then carefully study the 'Result' table to see this behavior in action with sample data.
To summarize the key behaviors you just read about:
ROW_NUMBER(): Assigns a unique, sequential integer to every row. It doesn't care about ties; it just keeps counting.RANK(): Assigns the same rank to tied rows. However, it then skips the next rank(s) to account for the number of tied rows. This creates gaps in the ranking sequence.DENSE_RANK(): Assigns the same rank to tied rows, but it does not skip any ranks. The sequence remains consecutive, or "dense."
This animated GIF provides an excellent visual summary of these differences. Watch how the ranks are assigned for the two reps with sales of 300.

When to use which?
- Use
ROW_NUMBER()when you need a unique identifier for each row, like for de-duplication or selecting an arbitrary "first" row. - Use
RANK()for traditional competition-style rankings, like "Olympic medal standings," where two people can tie for silver, and no one gets bronze. - Use
DENSE_RANK()when you want a gapless ranking, which is common for creating leaderboards like "Top 10 products."
3. Ranking within Groups: PARTITION BY
Ranking an entire table is useful, but the true power of window functions is revealed when you rank data within specific groups. This is where the PARTITION BY clause comes in. It splits the dataset into separate "windows," and the ranking function resets and runs independently for each one.

Let's see this in action with our ranking functions.
SQL Ranking Window Functions With Examples
The DataLemur article you'll read next provides a clear example of ranking data within partitions.
Read the section titled "Window Function Ranking with PARTITION BY and ORDER BY". Observe how the query partitions the data by country and then ranks artists within each country. Pay close attention to the row_num, rank_num, and dense_rank_num columns in the result table and see how the rankings restart for each new country.
As you saw in the example, PARTITION BY country told the database to treat artists from the 'United States' as one group, 'Canada' as another, and so on. The ranking was then applied to the artists within each of those country groups. This is an incredibly common and powerful pattern for analysis.
4. Practice: Combining CTEs and Window Functions
A crucial rule to remember is that you cannot use a window function directly in a WHERE or HAVING clause. This is because of the logical order of operations in SQL: the WHERE clause is processed before the window functions are calculated.
So, how do you filter based on a rank, for example, to get the "Top 3"?
This is where your knowledge from the previous lesson comes in handy. The standard professional practice is to:
- Define a CTE that calculates the rank using a window function.
- Write the final
SELECTstatement that queries the CTE and applies aWHEREclause to the newly created rank column.
Let's put this all together in an exercise.
Scenario:
You are an analyst at an e-commerce company. The marketing team wants to know the top two best-selling products (by quantity sold) within each product category.
Tables:
products(product_id,product_name,category,price)orders(order_id,product_id,quantity)
Your Task:
Write a query that returns the category, product_name, and total_quantity_sold for the top 2 products in each category. Use DENSE_RANK() for the ranking.
Steps:
- First, you need to calculate the total quantity sold for each product. You'll likely need to join
ordersandproductsand thenGROUP BYproduct_id,product_name, andcategory. - Create a CTE based on this initial aggregation.
- Inside the CTE, add a new column using the
DENSE_RANK()window function. You want to rank products within eachcategorybased on their total quantity sold (in descending order). - Finally, write a
SELECTstatement that queries your CTE and filters for rowsWHEREthe rank is less than or equal to 2.
Click here for a possible solution
WITH ProductSales AS (
-- Step 1: Calculate total quantity sold for each product
SELECT
p.category,
p.product_name,
SUM(o.quantity) AS total_quantity_sold
FROM
products AS p
JOIN
orders AS o ON p.product_id = o.product_id
GROUP BY
p.category,
p.product_name
),
RankedProductSales AS (
-- Step 2: Rank products within each category based on sales
SELECT
category,
product_name,
total_quantity_sold,
DENSE_RANK() OVER (PARTITION BY category ORDER BY total_quantity_sold DESC) AS rnk
FROM
ProductSales
)
-- Step 3: Filter for the top 2 ranked products in each category
SELECT
category,
product_name,
total_quantity_sold
FROM
RankedProductSales
WHERE
rnk <= 2
ORDER BY
category,
total_quantity_sold DESC;
Explanation:
This query uses two CTEs for maximum clarity. The first, ProductSales, handles the initial aggregation. The second, RankedProductSales, takes those results and applies the ranking. The final SELECT is then a simple filtering operation. This step-by-step approach is easy to read, debug, and maintain.
Conclusion
In this lesson, you've added a powerful set of tools to your SQL arsenal. Window functions for ranking are essential for creating leaderboards, finding top performers, de-duplicating data, and much more.
Key Takeaways:
- Window functions perform calculations on a set of rows without collapsing them.
ROW_NUMBER(),RANK(), andDENSE_RANK()are used for ranking, differing primarily in how they handle ties.- The
PARTITION BYclause is used to create separate "windows" or groups, allowing you to apply functions likeRANK()independently to each group. - You cannot filter on a window function in a
WHEREclause. The standard pattern is to calculate the rank in a CTE and then filter on the rank column in the outer query.
In our next lesson, we will continue exploring window functions. We will move beyond ranking to learn how to calculate running totals and moving averages, which are fundamental for trend analysis. This will introduce you to the concept of a "frame clause" (ROWS / RANGE), which gives you even more control over your window.