Course topics

By WebNest Studio Engineering Team

PostgreSQL Tutorial

Window Functions: ROW_NUMBER, RANK, and PARTITION BY

Window functions perform a calculation across a set of rows related to the current row — without collapsing those rows into a single output row the way GROUP BY does. This lets you compute rankings, running totals, and per-group comparisons while still returning every original row, which is something a plain aggregate query cannot do.

The syntax always follows the same shape: a function call followed by an OVER (...) clause that defines the "window" — optionally partitioned into groups and ordered within each group.

Why it matters

"The top three products in each category", "each sale as a percentage of its month's total" and "a running balance" all need a calculation across related rows without collapsing them. Window functions do exactly that. They replace clumsy self-joins and are a favourite of interviewers for data and back-end roles.

ROW_NUMBER, RANK, and DENSE_RANK

ROW_NUMBER() assigns a strictly increasing, unique number to each row within its window, even if values tie. RANK() assigns the same rank to tied rows but then skips the next rank number(s) (1, 2, 2, 4). DENSE_RANK() also gives tied rows the same rank but never skips a number (1, 2, 2, 3). Choosing between them depends on whether gaps after ties are meaningful for your use case.

PARTITION BY: Resetting the Window per Group

PARTITION BY column inside the OVER clause restarts the window function's calculation independently for each distinct value of that column — for example, ranking products within each category separately rather than across the whole table. Without PARTITION BY, the window spans the entire result set. You combine it with ORDER BY inside the same OVER clause to control the ranking order within each partition.

A Concrete Ranking Example

A very common real-world pattern is "top N per group" — for example, the two highest-priced products in each category. Window functions make this a single clean query: rank products within each category by price, then filter on that rank in an outer query (since window functions cannot be used directly inside a WHERE clause).

Examples

Ranking products by price within each category using PARTITION BY

SQL
SELECT
    name,
    category_id,
    price,
    RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS price_rank
FROM products;
Output
      name       | category_id | price  | price_rank
------------------+-------------+--------+------------
 UltraBook 14     |           1 | 999.00 |          1
 Budget Laptop    |           1 | 399.00 |          2
 Wireless Mouse   |           2 |  29.99 |          1

Top 2 most expensive products per category using ROW_NUMBER in a CTE

SQL
WITH ranked AS (
    SELECT name, category_id, price,
           ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn
    FROM products
)
SELECT name, category_id, price FROM ranked WHERE rn <= 2;
Output
      name       | category_id | price
------------------+-------------+--------
 UltraBook 14     |           1 | 999.00
 Budget Laptop    |           1 | 399.00
 Wireless Mouse   |           2 |  29.99

Common Mistakes

  • Trying to filter directly on a window function in a WHERE clause (e.g. "WHERE RANK() = 1") — window functions are evaluated after WHERE, so you must wrap the query in a CTE or subquery and filter in the outer query.
  • Using RANK() when DENSE_RANK() (or vice versa) was actually intended, producing unexpected gaps or lack thereof after tied values.
  • Forgetting PARTITION BY entirely and getting a single global ranking when a per-group ranking was intended.
  • Confusing window functions with GROUP BY aggregates — window functions keep every row in the output, while GROUP BY collapses rows into one per group.

Exercise

Return the most expensive product in each category: the category, the product name and its price. Use ROW_NUMBER() partitioned by category, and order the final result by category.

Expected output: category | name | price, books | Atlas | 900, pens | Gold pen | 300 (one per line)

SQL · starter code
CREATE TABLE products (id INTEGER PRIMARY KEY, category TEXT, name TEXT, price INTEGER);
INSERT INTO products (id, category, name, price) VALUES
  (1, 'pens', 'Ball pen', 12),
  (2, 'pens', 'Gold pen', 300),
  (3, 'books', 'Atlas', 900),
  (4, 'books', 'Notebook', 60);

-- TODO: the most expensive product of each category
Show hints
  • Number the rows inside each category: ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC).
  • A window function cannot be used in WHERE, so compute it in a CTE or subquery and filter on it outside.
Show solution
SQL · solution
CREATE TABLE products (id INTEGER PRIMARY KEY, category TEXT, name TEXT, price INTEGER);
INSERT INTO products (id, category, name, price) VALUES
  (1, 'pens', 'Ball pen', 12),
  (2, 'pens', 'Gold pen', 300),
  (3, 'books', 'Atlas', 900),
  (4, 'books', 'Notebook', 60);

WITH ranked AS (
  SELECT category, name, price,
         ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS position
  FROM products
)
SELECT category, name, price
FROM ranked
WHERE position = 1
ORDER BY category;

Quick quiz

1. How does a window function differ from GROUP BY?

  • A. It is the same thing
  • B. It can only count
  • C. It removes duplicate rows
  • D. It keeps every row and adds a value computed over related rows; GROUP BY collapses each group into one row
Show answer

D. The detail rows stay available beside the aggregate.

2. Three rows tie for first place. What do RANK() and DENSE_RANK() give the next row?

  • A. RANK gives 4; DENSE_RANK gives 2
  • B. Both give 2
  • C. Both give 4
  • D. RANK gives 2; DENSE_RANK gives 4
Show answer

A. RANK leaves gaps after ties, and DENSE_RANK does not. ROW_NUMBER gives every row a different number.

3. What does PARTITION BY do in an OVER clause?

  • A. Sorts the final result
  • B. Splits the rows into groups, and the function restarts for each group
  • C. Filters rows
  • D. Creates a new table
Show answer

B. Without it, the window is the whole result set.

Interview questions

What is a window function?

Show answer

A window function computes a value for each row from a set of rows related to it, the window, defined by OVER. PARTITION BY divides the rows into groups and ORDER BY orders them within each group. Unlike an aggregate with GROUP BY, it does not reduce the number of rows. Examples are ROW_NUMBER, RANK, LAG and LEAD, and aggregates such as SUM used with OVER for running totals.

How do you get the top N rows in each group?

Show answer

Number the rows within each group with ROW_NUMBER() OVER (PARTITION BY group_column ORDER BY sort_column DESC) in a CTE or subquery, then filter the outer query to rows whose number is at most N. The filter has to be outside because window functions are evaluated after WHERE. Use RANK or DENSE_RANK instead when tied rows should all be included.

Key Points to Remember

  • Window functions compute per-row values across a related set of rows without collapsing the result set.
  • ROW_NUMBER gives unique sequential numbers; RANK skips numbers after ties; DENSE_RANK does not skip.
  • PARTITION BY restarts the window calculation independently for each group; ORDER BY controls ranking order within it.
  • To filter on a window function's result, wrap the query in a CTE or subquery, since window functions cannot appear in WHERE directly.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering