Advanced SQL

SQL Window Functions: ROW_NUMBER, RANK & PARTITION BY

8 min read · Updated August 28, 2026

A note on running these examples

Our in-browser engine supports only part of the window-function syntax, so this page is honest about it: examples marked ▶ runs here work in the playground, and the rest need a full database — PostgreSQL, MySQL 8+, SQL Server, or SQLite 3.25+. Everything on this page is standard SQL that behaves identically across those systems.

A window function looks at other rows without collapsing them. GROUP BY answers "what is the average salary per department" by folding each department into one row. A window function answers "show me every employee, each with their department's average alongside" — all the rows survive, and each gets a value computed over its "window" of related rows. That one idea powers rankings, running totals, and the top-N-per-group questions that appear in almost every serious SQL interview.

Numbering rows: ROW_NUMBER ▶ runs here

SELECT ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,
       first_name, salary
FROM Employees;

OVER is what makes a function a window function. Here the window is the whole table ordered by salary, and ROW_NUMBER hands out 1, 2, 3… down that order. Unlike ORDER BY alone, the position becomes a real value you can filter on — which is how "give me rows 11 to 20" pagination is built.

A group value on every row: AVG OVER PARTITION BY ▶ runs here

SELECT first_name, dept_id, salary,
       AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg
FROM Employees;

PARTITION BY is GROUP BY's non-destructive cousin: it splits the rows into per-department windows, computes the average within each, and stamps it on every row of that department. Twelve employees in, twelve rows out — each now carrying its own comparison point. Computing "who earns above their department's average" becomes a simple wrap of this query, no correlated subquery required.

Ranking with ties: RANK and DENSE_RANK

-- PostgreSQL / MySQL 8+ / SQL Server / SQLite 3.25+
SELECT first_name, salary,
       RANK()       OVER (ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM Employees;

All three numbering functions differ only in how they treat ties. ROW_NUMBER ignores them — equal rows still get different numbers, arbitrarily. RANK gives equal rows the same number and then skips (1, 2, 2, 4). DENSE_RANK gives equal rows the same number and does not skip (1, 2, 2, 3). Interviewers love asking for this difference; now you have it.

The interview classic: top earner per department

-- PostgreSQL / MySQL 8+ / SQL Server / SQLite 3.25+
WITH ranked AS (
  SELECT first_name, dept_id, salary,
         ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM Employees
)
SELECT first_name, dept_id, salary
FROM ranked
WHERE rn = 1;

"Highest paid employee in each department" — the question that separates people who know window functions from people who fight it with correlated subqueries. Rank within each partition, keep rank 1. Change rn = 1 to rn <= 3 and it is top three per department; that is the whole trick.

Running totals

-- PostgreSQL / MySQL 8+ / SQL Server / SQLite 3.25+
SELECT order_date, order_total,
       SUM(order_total) OVER (ORDER BY order_date) AS running_total
FROM Orders;

An aggregate with an ORDER BY inside OVER accumulates: each row's value is the sum of everything up to and including it. This is the cumulative-revenue chart every dashboard has, expressed in one line — and the same shape gives moving averages once you add a frame clause like ROWS BETWEEN 6 PRECEDING AND CURRENT ROW.

Try it yourself

The two examples marked as runnable work in the playground right now - try changing the ORDER BY column or the PARTITION BY column and watch the numbers follow.

Open the Playground

Window functions vs GROUP BY, in one sentence each

GROUP BY collapses rows to one per group and can only return group-level columns. Window functions keep every row and attach group-level (or position-level) values to each. When the question starts with "for each row…", it is a window function; when it starts with "how many / what total per group…", it is GROUP BY.

Frequently asked questions

What is a window function in SQL?

A function that computes a value for each row using other, related rows - its "window" - without collapsing them the way GROUP BY does. Rankings, running totals and per-group comparisons on every row are the classic uses.

What is the difference between ROW_NUMBER, RANK and DENSE_RANK?

They differ only on ties. ROW_NUMBER always gives distinct numbers, even to equal rows. RANK gives ties the same number and skips the next positions (1, 2, 2, 4). DENSE_RANK gives ties the same number without skipping (1, 2, 2, 3).

What does PARTITION BY do?

It splits the rows into separate windows - one per distinct value - and the function computes independently inside each, restarting at every boundary. It is GROUP BY that keeps all the rows.

How do I get the top N rows per group in SQL?

Rank the rows inside each group with ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col DESC) in a CTE, then keep the rows where the rank is at most N in the outer query.

Can I use a window function in a WHERE clause?

No - window functions are evaluated after WHERE. To filter on one, compute it in a CTE or subquery and filter in the outer query. That restriction is exactly why the top-N pattern is written in two steps.

Which databases support window functions?

PostgreSQL, SQL Server, Oracle, MySQL 8.0+ and SQLite 3.25+ all support the standard syntax used on this page. The in-browser engine on this site runs only a subset, which is why some examples here are marked as needing a full database.