Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Window Functions for Partitioned Analysis

Recommended resources

Hello! In our last lesson, we mastered CASE statements and Common Table Expressions (CTEs), which are fundamental for applying business logic and structuring your queries. You saw how CTEs, in particular, help break down complex problems into readable, logical steps.

Today, we'll build directly on that foundation by diving into one of the most powerful features in modern SQL: window functions. These functions allow you to perform calculations across a set of related rows—like ranking customers by their sales within each region—without collapsing them like a GROUP BY would. By the end of this lesson, you'll be able to use functions like RANK() and DENSE_RANK() with the PARTITION BY clause to perform exactly this kind of sophisticated, partitioned analysis.

This skill is a cornerstone of business intelligence and analytics, enabling you to answer critical questions like:

  • Who are our top 3 performing salespeople in each country?
  • What are the most popular products within each category?
  • How does each employee's salary compare to the average of their department?

Let's get started.

What is a Window Function?

First, it's crucial to understand how window functions differ from the aggregate functions (SUM, COUNT, AVG, etc.) you use with GROUP BY.

  • An aggregate function collapses multiple rows into a single summary row. If you GROUP BY region and SUM(sales), you get one row of total sales for each region. You lose the individual sales data.
  • A window function performs a calculation across a set of rows (the "window") but returns a value for every single row. You get to see the individual row's data and the result of the window calculation alongside it.

The Substack article, "SQL Window Functions Made Simple," puts it nicely: they’re like "peeking over the fence at your neighbor’s yard, seeing the big picture while staying in your row."

The basic syntax always includes the OVER() clause:
FUNCTION_NAME() OVER ( ... )

Let's see how this works by using a familiar aggregate function, MAX(), as a window function.

SQL Window Function | How to write SQL Query using RANK, DENSE RANK, LEAD/LAG | SQL Queries Tutorial

This video from techTFQ provides an excellent introduction. It starts by showing the limitations of GROUP BY and then introduces window functions as the solution. Pay close attention to how a normal aggregate function like MAX() behaves differently when paired with an OVER() clause.

Watch from 01:31 to 06:18. This section explains: The difference between a standard GROUP BY and using an aggregate function with OVER(). How the PARTITION BY clause is used within OVER() to create separate windows for calculation (e.g., finding the max salary per department).

As you saw, MAX(salary) OVER (PARTITION BY department_name) returns the maximum salary for a given department on every row for employees in that department. This is the core concept: we're adding analytical context without losing the original detail.


Ranking Rows with ROW_NUMBER, RANK, and DENSE_RANK

The most common use of window functions is for ranking. SQL provides three primary functions for this, and the main difference is how they handle ties (i.e., when two rows have the same value in the column you're ordering by).

  1. ROW_NUMBER(): Assigns a unique, sequential number to each row. It never repeats numbers, even for ties.
  2. RANK(): Assigns the same rank to tied rows. However, it then skips the next rank(s). For example, if two rows are tied for 2nd place, they both get rank 2, and the next rank assigned is 4.
  3. DENSE_RANK(): Assigns the same rank to tied rows, but does not skip the next rank. In the same example, two rows get rank 2, and the next rank assigned is 3.

Let's review these three functions in a clear, concise article.

SQL Ranking Window Functions With Examples

This DataLemur article clearly defines and contrasts the three main ranking functions. Focus on understanding the distinct behavior of each.

Read the sections 'ROW_NUMBER vs. RANK vs. DENSE_RANK' and 'Window Function Ranking with ORDER BY'. Pay close attention to the example table that shows concert revenue and the resulting columns for row_num, rank_num, and dense_rank_num. Notice how they differ when revenues are tied.

This image provides a perfect visual summary of how the three functions treat ties within different groups.

Comparison of SQL Ranking Window Functions
This table clearly illustrates how `ROW_NUMBER` always increments, `RANK` assigns the same value to ties and then skips, and `DENSE_RANK` assigns the same value to ties without skipping, all within partitioned groups 'A' and 'B'.

Choosing the right function depends on your business question:

  • Use ROW_NUMBER() if you need a unique number for every row, regardless of ties (e.g., for picking an arbitrary "top N").
  • Use RANK() for traditional competition-style ranking where ties share a rank and subsequent positions are skipped.
  • Use DENSE_RANK() when you want a gapless ranking, which is often useful for "top N" reports where you want all items at a certain rank level, even if it means including more than N items due to ties.

The Power of Partitioning: Ranking Within Groups

Now we get to the core of today's learning outcome: performing partitioned analysis. This is done by combining PARTITION BY with ORDER BY inside the OVER() clause.

OVER (PARTITION BY column_to_group BY ORDER BY column_to_rank_by)

This tells SQL to:

  1. First, split the data into groups or "partitions" based on the PARTITION BY column (e.g., region, department).
  2. Then, within each of those partitions, order the rows based on the ORDER BY column (e.g., sales DESC).
  3. Finally, apply the window function (e.g., RANK()) to the ordered rows within each partition. The ranking will reset for each new partition.

This is exactly what you need to rank customers within regions.

SQL Window Functions Made Simple

This article from Substack provides a very clear, practical example of partitioning. It directly addresses a common business request and shows how to combine partitioning with a CTE to get the final answer.

Read the section titled 'PARTITION'. The example shows how to find the top salesperson per country. Notice how the ranking is done within each country partition. The use of a CTE to then select only the top-ranked sellers is a direct application of what you learned in the previous lesson.

The query and output below show another perfect example of this in action.

SQL RANK() Window Function with PARTITION BY Example
This image shows a query that ranks salesperson amounts `PARTITION BY region`. In the output, you can see the rank resets from 'North' to 'South', demonstrating partitioned ranking.

Putting It All Together: A Complete Example

Let's walk through a common business scenario: Find the top 3 highest-paid employees in each department.

To solve this, you need to:

  1. Partition the data by department.
  2. Order the data by salary in descending order within each department.
  3. Rank the employees using RANK() or DENSE_RANK().
  4. Filter the results to keep only those with a rank of 3 or less.

This video demonstrates exactly how to do this.

SQL Window Function | How to write SQL Query using RANK, DENSE RANK, LEAD/LAG | SQL Queries Tutorial

Let's return to the techTFQ video to see a full, practical application of ranking functions with partitioning. This directly models how you'll solve real-world analytical tasks.

Watch from 11:56 to 18:01. This section covers: Using RANK() partitioned by department and ordered by salary to find the top N employees. Using DENSE_RANK() for the same problem and clearly explaining the difference in output when ties are present. Wrapping the window function query in a subquery to filter for the top ranks. Note that this subquery could (and often should) be written as a CTE for readability!

Test your understanding!

You have a table named product_sales with columns: product_id, product_name, category, and sales_total.

Write a SQL query to find the top 2 best-selling products (by sales_total) within each category. Your result should include the category, product_name, sales_total, and the rank. Because you want to include all products tied for 2nd place, DENSE_RANK() is the most appropriate function.

Show answer

First, you would write a query with a window function to rank the products within each category. You could place this inside a CTE.

WITH RankedProducts AS (
  SELECT
    product_name,
    category,
    sales_total,
    DENSE_RANK() OVER (PARTITION BY category ORDER BY sales_total DESC) AS sales_rank
  FROM
    product_sales
)

Then, you would query that CTE to select only the rows where the rank is 1 or 2.

SELECT
  category,
  product_name,
  sales_total,
  sales_rank
FROM
  RankedProducts
WHERE
  sales_rank <= 2;

Conclusion

Congratulations! You've just unlocked a new tier of analytical capabilities in SQL. Window functions are essential for any data analyst, allowing you to create sophisticated rankings, comparisons, and aggregations that are simply not possible with GROUP BY alone.

Key Takeaways:

  • Window vs. Aggregate: Window functions perform calculations across a "window" of rows but preserve the individual rows, unlike GROUP BY which collapses them.
  • The OVER Clause: This is the heart of a window function. OVER(PARTITION BY ... ORDER BY ...) defines how the window is constructed.
  • Partitioning is Key: PARTITION BY divides your data into subgroups, and the window function restarts for each subgroup. This is how you analyze data "within" a category.
  • Ranking Functions:
    • ROW_NUMBER(): Unique, sequential numbers.
    • RANK(): Handles ties by skipping ranks.
    • DENSE_RANK(): Handles ties without skipping ranks.
  • CTEs and Window Functions: They are a powerful combination. Use a CTE to perform your window calculation and then SELECT from the CTE to filter, aggregate, or join the results.

Next Lesson Preview:

We've focused on ranking functions today, but the OVER() clause can be used with many other functions. In our next lesson, we will continue exploring window functions to calculate running totals and moving averages. This will enable you to analyze trends over time, such as calculating month-over-month growth or 7-day rolling average sales, all directly within SQL.