SQLWindow Functionsbeginner
Updated:

SQL Window Functions Introduction

6 min read

Window functions rank rows and compute running totals without collapsing your data. Learn OVER, PARTITION BY, ROW_NUMBER and RANK from scratch.

TL;DR – Quick Answer

A window function performs a calculation across a set of rows related to the current row, without collapsing them like GROUP BY does. Using the OVER clause with PARTITION BY and ORDER BY, functions like ROW_NUMBER, RANK, SUM and LAG add rankings and running totals alongside each original row. They solve ranking and top-N-per-group problems cleanly.

On This Page

Window functions are the feature that makes analysts reach for SQL instead of a spreadsheet. They let you rank rows, number them, and compute running totals — all while keeping every original row visible. Once they click, a whole class of "top N per group" and "running total" problems that felt impossible become two lines of SQL.

They build on aggregation, so make sure GROUP BY in SQL is solid first. The key contrast to hold in mind: GROUP BY collapses rows into summaries, while a window function keeps every row and adds a computed column beside it.

Our sample data

-- employees
emp_id | emp_name | department_id | salary
-------+----------+---------------+--------
101    | Aarti    | 1             | 78000
102    | Bhaskar  | 1             | 65000
103    | Chitra   | 2             | 52000
104    | Devan    | 2             | 58000
105    | Esha     | 3             | 47000
106    | Farhan   | 4             | 90000

The OVER clause: what makes a function a window function

Any aggregate becomes a window function when you add an OVER clause. Compare these two queries. The first collapses; the second does not.

-- GROUP BY: one row, loses detail
SELECT AVG(salary) AS company_avg
FROM employees;

-- Window: every row kept, average shown beside each
SELECT emp_name,
       salary,
       AVG(salary) OVER () AS company_avg
FROM employees;

The second query returns all six employees, each with an extra column holding the company average. The empty OVER () means "the window is the entire result set." This is already useful — now every row can be compared to the overall average without a subquery.

PARTITION BY: windows within groups

An empty window spans all rows. PARTITION BY splits the rows into groups and restarts the calculation within each one — like GROUP BY, but without collapsing. To show each employee's salary next to their department's average:

SELECT emp_name,
       department_id,
       salary,
       AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
FROM employees;

Now Aarti and Bhaskar (both department 1) share a dept_avg of 71500, while Chitra and Devan (department 2) share 55000. Every row survives, and each carries its own department's average. Achieving this with GROUP BY alone is impossible because grouping would destroy the individual rows.

Pro tip: Read OVER (PARTITION BY x) as "for each group of x, compute this." It is the mental bridge from GROUP BY to window functions — same grouping idea, but the detail rows stay.

Ranking functions: ROW_NUMBER, RANK, DENSE_RANK

The most-used window functions assign a position to each row. Add ORDER BY inside OVER to define what "position" means:

SELECT emp_name,
       salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
       RANK()       OVER (ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;

The three differ only in how they treat ties. Suppose two employees earned the same salary:

Function Behavior on ties Example sequence
ROW_NUMBER Always unique, arbitrary tie-break 1, 2, 3, 4
RANK Ties share a rank, then a gap 1, 1, 3, 4
DENSE_RANK Ties share a rank, no gap 1, 1, 2, 3

Knowing which to use is a frequent interview checkpoint. ROW_NUMBER for "pick exactly one per group," RANK when gaps after ties are acceptable, DENSE_RANK when they are not.

Interview note: "Difference between RANK and DENSE_RANK?" is asked constantly. The one-line answer: RANK leaves gaps after ties (1,1,3), DENSE_RANK does not (1,1,2). Add that ROW_NUMBER never ties, and you have covered the whole family.

Top-N-per-group: the killer use case

Here is the problem window functions were made for: "the highest-paid employee in each department." With PARTITION BY plus ROW_NUMBER, it is direct:

SELECT emp_name, department_id, salary
FROM (
    SELECT emp_name,
           department_id,
           salary,
           ROW_NUMBER() OVER (
               PARTITION BY department_id
               ORDER BY salary DESC
           ) AS rn
    FROM employees
) ranked
WHERE rn = 1;

Inside each department the rows are ranked by salary, then the outer query keeps only rank 1. Doing this with a correlated subquery is possible but clumsier, which is why window functions are the modern default for ranking problems.

Running totals with aggregate windows

Add ORDER BY to an aggregate window and it becomes a running calculation — accumulating as it goes. A running total of salary, ordered by employee id:

SELECT emp_name,
       salary,
       SUM(salary) OVER (ORDER BY emp_id) AS running_total
FROM employees;

Each row's running_total is the sum of all salaries up to and including that row. Combine with PARTITION BY to reset the running total per department. Running totals, moving averages and cumulative counts all follow this pattern, and they are staples of financial and analytics reporting.

LAG and LEAD: peeking at neighbor rows

LAG and LEAD let a row see the value from a previous or next row — perfect for period-over-period comparisons:

SELECT emp_name,
       salary,
       LAG(salary) OVER (ORDER BY salary) AS prev_salary,
       salary - LAG(salary) OVER (ORDER BY salary) AS gap
FROM employees;

Ordered by salary, each row shows the salary just below it and the gap. In real data this pattern computes month-over-month growth, day-over-day change, and similar deltas without a self-join.

NTILE: splitting rows into buckets

NTILE(n) divides the ordered rows into n roughly equal buckets and labels each row with its bucket number. It is how you build quartiles, deciles and percentile bands:

SELECT emp_name,
       salary,
       NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;

The highest earners land in quartile 1, the lowest in quartile 4. Analysts use NTILE constantly to segment customers by spend or employees by performance without hard-coding thresholds. Because it partitions by rank position rather than value, the buckets stay balanced even as the data changes.

Named windows and reuse

When several window functions share the same OVER definition, repeating it is noisy and error-prone. The WINDOW clause lets you name a window once and reuse it:

SELECT emp_name,
       salary,
       RANK()       OVER w AS rnk,
       DENSE_RANK() OVER w AS dense_rnk,
       ROW_NUMBER() OVER w AS row_num
FROM employees
WINDOW w AS (ORDER BY salary DESC);

All three functions reference the named window w, keeping the query clean and guaranteeing they use identical ordering. Both PostgreSQL and MySQL 8.0+ support named windows. It is a small feature that pays off as your analytical queries grow.

Common mistakes

Confusing window ORDER BY with the query's ORDER BY. The ORDER BY inside OVER (...) controls the window calculation, not the final row order. You often need a separate ORDER BY at the end of the query to sort the output.

Trying to filter on a window column in WHERE. Window functions are computed after WHERE, so you cannot reference rn in a WHERE clause of the same query level. That is exactly why the top-N example wraps the window query in a subquery and filters in the outer query.

Common mistake: Writing WHERE ROW_NUMBER() OVER (...) = 1 and getting an error. Window functions run late in the execution order, after WHERE. Compute the window in a subquery or CTE, then filter its result in the outer query.

Window functions versus GROUP BY

The final distinction, stated plainly:

  • Use GROUP BY when you want a summary and do not need the individual rows — total sales per region, headcount per department.
  • Use a window function when you need the summary and the detail together — each sale next to its region's total, each employee next to their department's rank.

Mastering when to pick each is a sign of real SQL fluency, and it is a skill the data analyst roadmap leans on heavily.

Where to go next

Window functions round out the core of analytical SQL. With OVER, PARTITION BY and the ranking functions, you can solve ranking, running-total and neighbor-comparison problems that stump beginners.

Revisit subqueries in SQL to see how the older approaches compare, then apply everything on the classic second-highest-salary problem, which has an elegant window-function solution. From here, the whole SQL learning hub opens up toward indexes, transactions and design.

Frequently Asked Questions

What is a window function in SQL?
A window function computes a value over a window of rows defined by the OVER clause, while keeping every individual row in the output. Unlike GROUP BY, it does not merge rows into summaries. This lets you show a ranking or running total next to each detail row.
What is the difference between RANK and DENSE_RANK?
RANK leaves gaps after ties, so two rows tied at rank 1 are followed by rank 3. DENSE_RANK does not leave gaps, so the next rank is 2. ROW_NUMBER never ties and assigns a unique sequential number to every row.
What does PARTITION BY do?
PARTITION BY divides the rows into groups and restarts the window calculation within each group, similar to GROUP BY but without collapsing rows. For example, PARTITION BY department_id ranks salaries separately inside each department. Without it, the window spans the entire result set.
What is the difference between GROUP BY and window functions?
GROUP BY reduces many rows to one summary row per group, losing individual detail. A window function keeps all rows and adds the computed value as an extra column. Use GROUP BY for summaries and window functions when you need detail plus aggregate together.
Do MySQL and PostgreSQL support window functions?
PostgreSQL has supported window functions for many years, and MySQL added them in version 8.0. Both support ROW_NUMBER, RANK, DENSE_RANK and aggregate windows with OVER. On MySQL versions before 8.0 you must emulate them with subqueries or variables.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us