Advanced Window Functions: Running Totals, Ranking, and Analytics
Master advanced window functions to calculate running totals, rankings, and lag comparisons in PostgreSQL without losing your row-level data.

Previously in this course, we covered Introduction to Aggregate Functions: SQL Math for Analytics to perform grand totals and summary calculations. While useful, aggregate functions collapse your data; if you group by a category, you lose the ability to see individual records.
Window functions provide the bridge between row-level detail and aggregate insights. They perform calculations across a set of rows related to the current row, but—crucially—they do not group the result set into a single line.
Understanding the Window Function Syntax
The core of a window function is the OVER() clause. This tells PostgreSQL that the function should operate over a "window" of rows rather than the entire table.
SQLSELECT column_name, FUNCTION() OVER (PARTITION BY column_name ORDER BY column_name) FROM table_name;
- PARTITION BY: Divides the rows into groups (e.g., sales per store).
- ORDER BY: Defines the sequence in which the calculation is applied (crucial for running totals and rankings).
Calculating Running Totals

A running total—the cumulative sum of values over time—is a classic analytical requirement. Using standard aggregation, you would need complex self-joins, but with window functions, it’s a single line of code.
Imagine our store database has a sales table. We want to see a running total of revenue for each product:
SQLSELECT sale_date, amount, SUM(amount) OVER (ORDER BY sale_date) as running_total FROM sales;
By omitting PARTITION BY, the window covers the entire table. By including ORDER BY sale_date, the SUM function adds each new row's amount to the previous total as it traverses the result set.
Ranking Data for Performance Insights
Ranking is essential for identifying top performers. Unlike standard sorting, RANK() allows us to see where a specific record sits relative to others in a dataset.
If we want to rank our sales by the amount sold, we use:
SQLSELECT product_name, amount, RANK() OVER (ORDER BY amount DESC) as sales_rank FROM sales;
This assigns a rank of 1 to the highest sale. If two sales are tied for the top spot, RANK() will give them both 1 and skip the next rank (e.g., the next record would be 3). If you prefer to avoid gaps in numbering, you can use DENSE_RANK() instead.
Comparing Rows with LAG and LEAD
Sometimes you need to compare a current row to the one before or after it. LAG() retrieves data from a previous row, while LEAD() looks ahead. This is perfect for calculating "Day-over-Day" sales growth.
SQLSELECT sale_date, amount, LAG(amount) OVER (ORDER BY sale_date) as previous_day_amount, amount - LAG(amount) OVER (ORDER BY sale_date) as daily_diff FROM sales;
This query shows today's sales alongside yesterday's, allowing you to calculate the delta instantly. As noted in our discussion on SQL Window Functions: MySQL vs PostgreSQL Performance Tuning, mastering these functions is a key step toward writing performant analytical queries.
Hands-on Exercise
Using your store database, write a query for the orders table that:
- Calculates the total price of each order.
- Uses
RANK()to rank orders by total price in descending order. - Uses
LAG()to show the price of the order placed immediately before the current one.
Common Pitfalls
- Performance Overhead: While powerful, window functions require the database to sort the data. On very large tables, ensure you have appropriate indexes on the columns used in your
PARTITION BYandORDER BYclauses. - The "Aggregate" Trap: You cannot use window functions in a
WHEREclause. TheWHEREclause is processed before the window function is calculated. If you need to filter based on a window function result, wrap your query in a CTE or a subquery. - Order Matters: For
SUM()orLAG(), omitting theORDER BYclause insideOVER()produces results that are often non-deterministic or logically incorrect for your use case.
FAQ
Q: What is the difference between PARTITION BY and GROUP BY?
A: GROUP BY collapses rows into a single summary row. PARTITION BY keeps all original rows and adds the calculated aggregate value as a new column for every row.
Q: Can I use multiple window functions in one query?
A: Yes, you can calculate a running total, a rank, and a lag in the same SELECT statement.
Q: Does RANK() handle ties?
A: Yes, RANK() assigns the same number to tied rows and skips the next number. ROW_NUMBER() would assign unique, sequential numbers regardless of ties.
Recap
Window functions allow you to perform sophisticated analytics while keeping your data granular. By using OVER, PARTITION BY, and ORDER BY, you can calculate running totals, identify rankings, and compare temporal trends with minimal code.
Up next: We will explore how to handle complex time-based data in Dealing with Time Zones.
Work with me

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.

AI Automation & Agentic Workflow Development
Automate the repetitive work eating your time — content pipelines, data workflows, and agentic AI tasks that run themselves.


