Skip to main content

Welcome to a Grasp lesson

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

Lesson 61

Window Functions: Partitioning Data

Recommended resources

Hello! Welcome back to our series on Advanced Analysis with Window Functions.

In our last lesson, we introduced the OVER() clause and saw how it transforms aggregate functions, allowing us to perform calculations across a set of rows without losing the original row-level detail. We also had a first look at PARTITION BY as a way to create logical groups for these calculations.

Today, we're going to take a much deeper dive into that concept. The learning outcome for this lesson is to define a data window using PARTITION BY to perform calculations on specific groups. We will solidify your understanding of how this clause works, explore partitioning by single and multiple columns, and see how to use different partitions within a single query. Mastering PARTITION BY is the key to unlocking the full analytical power of window functions.

Recapping the Core Idea: What is a Partition?

Let's start by reinforcing the fundamental concept. A window function operates on a "window" of rows. The PARTITION BY clause is what defines these windows. It breaks up the entire result set into smaller, non-overlapping subsets, or partitions. The window function is then applied to each partition independently.

Think of it like sorting physical mail. You might first sort all letters into bins by state (PARTITION BY state). The set of letters in the "California" bin is one partition, and the set in the "New York" bin is another. Any operation you perform (like counting the letters) happens independently within each bin.

SQL Window Function: PARTITION BY Example
This image shows an employee table being split into three distinct partitions based on `departmentId`. An aggregate function, like `AVG(salary)`, would be calculated independently for each of these three groups.

Why PARTITION BY is Essential: Doing what GROUP BY Can't

In the previous lesson, we established that GROUP BY collapses rows, while window functions don't. Let's look at a scenario where this distinction becomes critical and demonstrates the unique power of PARTITION BY.

Imagine you need to produce a report that shows every single sale, but also includes the total sales for the product sold in that transaction. How would you do this? If you use GROUP BY product_id, you'll get the total sales per product, but you'll lose the details of the individual sales.

This is a classic problem that window functions are designed to solve. The following video demonstrates this exact limitation of GROUP BY and then shows how a window function with PARTITION BY elegantly solves it.

SQL Window Functions Basics (Visually Explained) | PARTITION BY, ORDER BY, FRAME | #SQL Course 22

The 'Data with Baraa' channel provides a clear, step-by-step example of a problem that is difficult with GROUP BY but straightforward with PARTITION BY.

Please watch from 07:31 to 12:11. First, notice how trying to add detail columns to a GROUP BY query fails or produces incorrect results (07:31 - 09:37). Then, see how using SUM(...) OVER (PARTITION BY ...) achieves the desired outcome perfectly (09:37 - 12:11).

As the video shows, the solution is to use an aggregate function as a window function:

SELECT
  order_id,
  product_id,
  sale_amount,
  SUM(sale_amount) OVER (PARTITION BY product_id) AS total_sales_for_product
FROM sales;

For every single sales row, this query looks at the product_id, finds all other rows in the same partition (i.e., with the same product_id), calculates their SUM(sale_amount), and displays that total in the new column. The calculation is confined to the "window" defined by the PARTITION BY clause.


Dissecting PARTITION BY

Let's get more formal. The PARTITION BY clause is a sub-clause of OVER(). Its role is to define the logical groups for the window function.

PostgreSQL Documentation: Window Functions

The official PostgreSQL documentation provides a concise and accurate definition. Let's read their explanation.

Read the first few paragraphs, starting from 'A window function performs a calculation...' down to '...the window function is computed across the rows that fall into the same partition as the current row.' Focus on how it explicitly defines the role of PARTITION BY in dividing rows into groups.

If you omit the PARTITION BY clause entirely (like we did with the empty OVER() in the last lesson), the database treats the entire result set as a single, massive partition.

Partitioning by a Single Column

This is the most common use case. You are grouping your data based on the values in one column, like department_id, customer_id, or region.

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

Let's watch a quick, practical example from the 'techTFQ' channel on how to find the max salary within each department.

Watch from 05:03 to 06:18. Observe how PARTITION BY department_name creates a window for each department, and the MAX(salary) function operates independently within each of these windows.

Partitioning by Multiple Columns

What if your grouping logic is more complex? For example, you might need to calculate the total sales for each salesperson, but broken down by year. In this case, you need to partition by both salesperson_id and the year of the sale.

You can achieve this by simply providing a comma-separated list of columns in the PARTITION BY clause.

... OVER (PARTITION BY salesperson_id, YEAR(sale_date))

When you partition by multiple columns, a new partition is created for every unique combination of values from those columns.

Let's see this in action.

SQL Window Functions Basics (Visually Explained) | PARTITION BY, ORDER BY, FRAME | #SQL Course 22

Let's return to the 'Data with Baraa' video, which has an excellent example of multi-column partitioning.

Watch from 25:52 to 27:54. The task is to find the total sales for each combination of product and order status. Notice how the window is defined by PARTITION BY product_id, order_status and how the resulting calculation is more granular than partitioning by product alone.

Test your understanding!

You have a table named support_tickets with the following columns: ticket_id, agent_id, customer_id, category (e.g., 'Billing', 'Technical'), and resolution_time_hours.

Write a query to retrieve all ticket details. In addition, include a column named avg_category_time_for_agent that shows the average resolution time for that agent within that specific ticket category.

Show answer
SELECT
  ticket_id,
  agent_id,
  customer_id,
  category,
  resolution_time_hours,
  AVG(resolution_time_hours) OVER (PARTITION BY agent_id, category) AS avg_category_time_for_agent
FROM support_tickets;

This query correctly partitions the data by the unique combination of agent_id and category. For a ticket handled by Agent 101 in the 'Billing' category, the window function will calculate the average resolution time of all other tickets handled by Agent 101 that are also in the 'Billing' category.


Advanced Usage: Multiple Partitions in One Query

One of the most powerful features of window functions, which truly sets them apart from GROUP BY, is the ability to define multiple, independent windows in the same SELECT statement. You can calculate a department-level summary and a company-wide summary in the same query, on the same line.

Window Function Basics - PARTITION BY

This article from SQLServerCentral.com provides a fantastic, in-depth look at PARTITION BY. We will focus on the final section, which demonstrates this powerful capability.

Read the final section of the article, starting from the text 'One other thing to keep in mind is that we can have separate partitions for different aggregates.' Focus on the example query that calculates a MAX(duedate) partitioned by CustomerID and a MIN(CustomerID) partitioned by DueDate in the same statement. This is something that would be very cumbersome without window functions.

As the article explains, a query like this is perfectly valid:

SELECT
  employee_id,
  department,
  salary,
  AVG(salary) OVER (PARTITION BY department) AS avg_dept_salary,
  AVG(salary) OVER () AS avg_company_salary
FROM employees;

This single query will show, for each employee:

  1. Their own salary.
  2. The average salary of their specific department.
  3. The average salary of the entire company.

Trying to achieve this with GROUP BY would require multiple subqueries or Common Table Expressions (CTEs) that would then need to be joined back together, making the query much more complex and potentially less performant.


Conclusion

In this lesson, we have thoroughly explored the PARTITION BY clause and its central role in defining the "windows" for our calculations.

Key Takeaways:

  • PARTITION BY is the primary mechanism for defining logical groups of rows (partitions) over which a window function operates.
  • Unlike GROUP BY, it allows you to compute group-level aggregates while preserving the individual detail of every row.
  • You can partition by a single column for simple grouping or by multiple columns for more granular, hierarchical grouping.
  • A single query can contain multiple window functions, each with its own unique PARTITION BY clause, enabling complex, multi-level analysis in a clean and efficient way.

Preview of the Next Lesson:
So far, our partitions have been treated as simple, unordered "bags" of rows. We calculate the sum or average of the entire bag. But what if the order of rows inside the partition matters?

In the next lesson, we will introduce the ORDER BY clause inside the OVER() clause. This will change the nature of our window, allowing us to perform sequence-aware calculations like running totals and moving averages. This will also pave the way for a whole new class of window functions: ranking functions like ROW_NUMBER(), RANK(), and DENSE_RANK().