DevLearningTools

LEARN · SQL

Window Functions

Window functions compute a value across a set of related rows without collapsing them into one, unlike GROUP BY. Learn ROW_NUMBER, RANK, DENSE_RANK, and PARTITION BY.

GROUP BY answers "what's the average salary per department," but it collapses every department down to one summary row, you lose the individual employees. A window function answers a different kind of question, "what's this employee's salary rank within their department," while keeping every individual row visible.

Learning Objectives

  • Explain how a window function differs from GROUP BY.
  • Rank rows with ROW_NUMBER, RANK, and DENSE_RANK, and explain how they handle ties differently.
  • Use PARTITION BY to restart a calculation within each group, without collapsing rows.

All Rows Kept

Unlike GROUP BY

PARTITION BY

Rows split into windows

Function Computed

Per row, within its window

Row Kept, Value Added

One new column, no rows lost

The Basic Shape: OVER()

Every window function is followed by OVER(...), which defines the "window" of rows it operates across. With no PARTITION BY, the window is the entire result set, every employee is ranked against everyone else. Critically, all three original rows are still here, nothing got collapsed.

MySQL / PostgreSQL / SQLite / SQL Server
SELECT first_name, department, salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS pay_rank
FROM employees;

-- Result:
-- first_name | department  | salary | pay_rank
-- Grace      | Engineering | 98000  | 1
-- Ada        | Engineering | 95000  | 2
-- Margaret   | Research    | 87000  | 3

ROW_NUMBER vs RANK vs DENSE_RANK

FunctionBehavior on a tie
ROW_NUMBER()Always gives a unique number, arbitrarily breaking ties
RANK()Ties get the same rank, the next rank skips ahead (1, 1, 3)
DENSE_RANK()Ties get the same rank, the next rank doesn't skip (1, 1, 2)
MySQL / PostgreSQL / SQLite / SQL Server
SELECT first_name, salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
  RANK() OVER (ORDER BY salary DESC) AS rank_num,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_num
FROM employees;

-- With two employees tied at 95000:
-- salary | row_num | rank_num | dense_rank_num
-- 98000  | 1       | 1        | 1
-- 95000  | 2       | 2        | 2
-- 95000  | 3       | 2        | 2
-- 87000  | 4       | 4        | 3

PARTITION BY: Restarting Per Group

PARTITION BY department splits the rows into separate windows, one per department, and the ranking restarts from 1 within each. This is the window-function equivalent of GROUP BY department, except every individual employee row is still here.

MySQL / PostgreSQL / SQLite / SQL Server
SELECT first_name, department, salary,
  RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;

-- Result:
-- first_name | department  | salary | dept_rank
-- Grace      | Engineering | 98000  | 1
-- Ada        | Engineering | 95000  | 2
-- Margaret   | Research    | 87000  | 1

Running Totals

Aggregate functions work as window functions too. Here, SUM() OVER (ORDER BY ...) computes a running total, each row's cumulative sum through that point in the ordering, rather than one grand total for the whole table.

MySQL / PostgreSQL / SQLite / SQL Server
SELECT first_name, salary,
  SUM(salary) OVER (ORDER BY salary DESC) AS running_total
FROM employees;

Frequently Asked Questions

Do all four databases in this course support window functions?

Yes, today. It's worth knowing the history though: MySQL only added window function support in version 8.0 (2018), and SQLite added it in version 3.25 (also 2018). Any MySQL server older than 8.0, still occasionally found in the wild, simply doesn't support them at all.

Can I use a window function's result in the same query's WHERE clause?

No, WHERE is evaluated before window functions are computed. To filter on a window function's result, wrap the query in a subquery or CTE and filter in the outer query instead.

What does LAG() and LEAD() do?

LAG(column) OVER (ORDER BY ...) returns the value from the previous row, LEAD(column) does the same for the next row, both useful for comparing a row to its neighbor, like calculating month-over-month change.

Common Beginner Mistakes

Trying to filter a window function's result with WHERE in the same query

WHERE runs before window functions are computed, so WHERE pay_rank = 1 after a ROW_NUMBER() window function fails. Wrap the query in a subquery or CTE, then filter in the outer query where the computed column is already available.

Using RANK() when ROW_NUMBER() (or DENSE_RANK()) was actually needed

RANK() skips numbers after a tie (1, 1, 3), which can look like a bug if you expected consecutive numbering. Pick DENSE_RANK() for consecutive ranks despite ties, or ROW_NUMBER() when ties genuinely need to be broken into a unique order.

Forgetting PARTITION BY and ranking across the whole table when a per-group rank was intended

Without PARTITION BY, OVER (ORDER BY salary DESC) ranks every row against the entire table, not within each department. The result compiles fine but silently answers the wrong question.

Best Practices

  • Reach for a window function whenever you need a per-row calculated value that still depends on other rows, ranking, running totals, comparisons to a previous row, without collapsing the result.
  • Choose RANK, DENSE_RANK, or ROW_NUMBER deliberately based on how ties should actually behave, don't default to one without thinking about it.
  • Filter a window function's output using a subquery or CTE, never directly in the same query's WHERE.
  • Check your database version before relying on window functions, especially on an older MySQL (pre-8.0) or SQLite (pre-3.25) instance.

Interview Questions

Write a query that shows the top 2 highest-paid employees in each department.

WITH ranked AS (SELECT first_name, department, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank FROM employees) SELECT first_name, department, salary FROM ranked WHERE dept_rank <= 2; — the window function is computed in a CTE first, since it can't be filtered directly in the same query's WHERE.

What's the key difference between a window function and GROUP BY?

GROUP BY collapses multiple rows into one summary row per group, you lose the individual rows. A window function computes a value across a set of related rows (optionally partitioned) but keeps every original row intact, adding the computed value as an extra column instead of replacing the rows.

Given salaries [98000, 95000, 95000, 87000], what would RANK() and DENSE_RANK() return for each, ordered descending?

RANK(): 1, 2, 2, 4 — the two ties both get rank 2, and the next rank skips to 4. DENSE_RANK(): 1, 2, 2, 3 — the ties still both get rank 2, but the next rank continues at 3 with no gap.

Summary

This lesson covered the OVER() clause, ranking rows with ROW_NUMBER, RANK, and DENSE_RANK and how each handles ties differently, splitting a calculation per group with PARTITION BY without collapsing rows, and running totals with an aggregate function used as a window function.

What's Next?

The next lesson covers Common Table Expressions (CTEs), naming a subquery with WITH for readability and reuse, including recursive CTEs.

</>

Ready to Practice SQL?

Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.

SQLite / PostgreSQL

Fast, no server, no ads.

Open DB Fiddle ↗

MySQL / SQL Server

Switch databases with one dropdown.

Open SQL Fiddle ↗

Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.