SQLWindow Functionsintermediate
Updated:

SQL Window Functions Interview Questions and Answers

7 min read

The window-function questions asked in modern SQL and analyst interviews — ROW_NUMBER vs RANK, PARTITION BY, running totals, LAG/LEAD and frames — answered properly.

TL;DR – Quick Answer

SQL window function interviews concentrate on the ranking family (ROW_NUMBER, RANK, DENSE_RANK), PARTITION BY vs GROUP BY, running totals and moving averages via frames, offset functions (LAG, LEAD), and the top-N-per-group pattern. Interviewers grade you on knowing that window functions keep every row while adding a computed column, and on writing them with correct partitioning and framing.

On This Page

Window functions are the single highest-signal SQL topic in modern analyst, data-engineering and backend interviews. They separate people who can only GROUP BY from people who can compute rankings, running totals and row-to-row comparisons — the calculations real reporting demands. If you learn one advanced SQL topic before an interview, make it this one.

The examples use sales(id, region, sale_date, amount) and standard SQL. Window functions are supported in PostgreSQL, MySQL 8+, SQL Server 2012+ and Oracle.

What is a window function?

A window function computes a value across a set of rows related to the current row — the "window" — while keeping every input row in the output. It is written with an OVER (...) clause that defines partitioning, ordering and framing.

The defining contrast with GROUP BY: aggregation collapses rows; a window function adds a column and keeps them all. Say that first — it reframes every later question correctly.

SELECT region, sale_date, amount,
       SUM(amount) OVER (PARTITION BY region) AS region_total
FROM sales;   -- every row kept, plus its region's total

Q1. Window function vs GROUP BY — explain the difference.

GROUP BY produces one output row per group; a window function produces one output row per input row, with the group calculation attached. If you need detail rows and a group value together, only the window function can do it in one pass.

-- GROUP BY: one row per region, detail lost
SELECT region, SUM(amount) FROM sales GROUP BY region;

-- Window: every sale, with its region total alongside
SELECT id, region, amount, SUM(amount) OVER (PARTITION BY region) AS region_total
FROM sales;

The killer example is "show each sale and what percent of its region it represents" — impossible with plain GROUP BY, trivial with amount / SUM(amount) OVER (PARTITION BY region). Reaching for that example demonstrates real fluency.

Interview note: Follow-up: "can you use both in one query?" Yes — a window function can operate over the results of a GROUP BY, since windowing is applied after grouping in the logical order.

Q2. ROW_NUMBER vs RANK vs DENSE_RANK — how do they differ on ties?

ROW_NUMBER always gives distinct numbers, breaking ties arbitrarily. RANK gives ties the same number then skips (1,1,3). DENSE_RANK gives ties the same number with no gap (1,1,2).

SELECT name, amount,
       ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn,
       RANK()       OVER (ORDER BY amount DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY amount DESC) AS drnk
FROM sales;

Pick by intent: "give me exactly the top 3 rows" wants ROW_NUMBER; "who tied for first" wants RANK or DENSE_RANK; "distinct ranking positions with no gaps" wants DENSE_RANK. Naming the tie behaviour precisely is the whole point of the question.

Interview note: Trap: "which one can produce more than N rows for top-N?" RANK and DENSE_RANK — because ties share a rank, WHERE rnk <= 3 can return four rows. ROW_NUMBER guarantees exactly N.

Q3. How do you get the top N per group?

Assign a per-group number with ROW_NUMBER() OVER (PARTITION BY group ORDER BY metric DESC) inside a subquery or CTE, then filter the outer query to <= N.

SELECT region, id, amount
FROM (
  SELECT region, id, amount,
         ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
  FROM sales
) ranked
WHERE rn <= 3;

The must-explain detail: you cannot put WHERE rn <= 3 in the inner query, because window functions are evaluated after WHERE in the logical processing order. The subquery/CTE wrapper is what gives you a place to filter. That explanation is exactly what interviewers listen for.

Interview note: Follow-up: "what if you want ties included at rank N?" Switch ROW_NUMBER to RANK or DENSE_RANK so tied rows at the boundary all appear.

Q4. How do you compute a running total?

Add ORDER BY inside the OVER clause; with an ordered window, an aggregate accumulates from the frame start to the current row. SUM(amount) OVER (ORDER BY sale_date) gives a running total.

SELECT sale_date, amount,
       SUM(amount) OVER (ORDER BY sale_date
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM sales;

The explicit frame matters: with ORDER BY and no frame, the default is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which lumps together rows with equal sale_date. Spelling out ROWS avoids that surprise. Showing you know the default frame is a senior-level detail.

Interview note: Trap: "why did my running total jump for same-date rows?" Because the default RANGE frame includes all peer rows with the equal ORDER BY value. Use ROWS for a strict row-by-row accumulation.

Q5. What do LAG and LEAD do?

LAG returns a value from a previous row, LEAD from a following row, within the partition and ordering — enabling row-to-row comparisons like period-over-period change without a self-join.

SELECT sale_date, amount,
       LAG(amount)  OVER (ORDER BY sale_date) AS prev_amount,
       amount - LAG(amount) OVER (ORDER BY sale_date) AS day_over_day
FROM sales;

Both take an optional offset and a default for the boundary rows (the first row has no previous). Before window functions, this required a correlated self-join; being able to say "LAG replaces the self-join" shows you understand what the feature is for.

Interview note: Follow-up: "what does LAG return for the first row?" NULL by default; pass a third argument as the default value, e.g. LAG(amount, 1, 0).

Q6. What is a window frame and when do you tune it?

A frame limits which rows within the partition the function considers for each current row. You tune it for moving windows — a 3-row moving average, a trailing-7-day sum — by specifying ROWS or RANGE bounds.

-- 3-row moving average
SELECT sale_date, amount,
       AVG(amount) OVER (ORDER BY sale_date
                         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3
FROM sales;

Frames apply to aggregate window functions (SUM, AVG, COUNT, MIN, MAX); ranking functions ignore frames. Knowing ROWS (physical row count) versus RANGE (value-based) is the distinction that comes up in follow-ups.

Interview note: Trap: "ROWS vs RANGE for a moving average?" ROWS counts exactly N physical rows; RANGE groups by value and can include more rows on ties. For a fixed N-row window, use ROWS.

Q7. Can you filter or aggregate on a window function's result directly?

Not in the same query level. Window functions are computed after WHERE, GROUP BY and HAVING, so you cannot reference the alias in those clauses — you must wrap the query in a subquery or CTE and filter outside.

-- Wrong: rn is not available in WHERE here
-- SELECT *, ROW_NUMBER() OVER (...) AS rn FROM sales WHERE rn = 1;   -- error

-- Right: filter in an outer query
WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
  FROM sales
)
SELECT * FROM ranked WHERE rn = 1;

This is a direct consequence of SQL's logical processing order, and interviewers love it because it tests whether you understand when windowing happens, not just how to write it.

Interview note: Follow-up: "where does windowing sit in the logical order?" After WHERE/GROUP BY/HAVING and before ORDER BY/DISTINCT. That ordering explains the wrapping requirement.

Q8. What is the difference between PARTITION BY and GROUP BY?

PARTITION BY divides rows into groups for the window function only, without collapsing them — every row stays. GROUP BY collapses each group to one row. PARTITION BY is "grouping without aggregation-collapse".

-- Rank employees within each region, keeping all rows
SELECT region, name, amount,
       RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS region_rank
FROM sales;

An empty/omitted PARTITION BY treats the whole result as one window. The mental model to offer: PARTITION BY resets the window at each group boundary but never removes rows — which is precisely why window functions and GROUP BY solve different problems.

Interview note: Trap: "can you PARTITION BY one column and ORDER BY another?" Yes — partitioning defines the groups, ordering defines sequence within each group; they are independent knobs.

How to prepare

Load a table with a date column and a few groups, then write, in order: a running total, a 7-row moving average, a LAG-based day-over-day change, and a top-3-per-group query. Those four cover the entire window-function interview surface, and writing the top-N query forces you to internalize why the window function must live in a subquery. Deliberately trigger the default-RANGE running-total surprise once so the frame rules stick.

Pair this with the aggregate functions questions, since window aggregates build directly on plain aggregates, and the subqueries set for the wrapping pattern the top-N query relies on. The SQL learning path covers the logical query order that explains why filtering on a window alias fails. A focused mock interview on analytical SQL is the best way to make these patterns automatic under pressure.

Frequently Asked Questions

What is the difference between a window function and GROUP BY?
GROUP BY collapses rows into one row per group, so detail is lost. A window function keeps every input row and adds a computed column that looks across a related set of rows. Use GROUP BY when you want aggregated rows and a window function when you want per-row values alongside a group calculation.
What is the difference between ROW_NUMBER, RANK and DENSE_RANK?
ROW_NUMBER assigns a unique sequential number even to ties. RANK gives tied rows the same rank but then skips numbers (1,1,3). DENSE_RANK gives ties the same rank with no gaps (1,1,2). Choose ROW_NUMBER for a strict order, RANK when gaps after ties are acceptable, DENSE_RANK when they are not.
How do you get the top N rows per group?
Use ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col DESC) in a subquery or CTE, then filter the outer query to rows where the number is <= N. You must wrap it because you cannot reference a window function in the same WHERE clause where it is computed.
What do LAG and LEAD do?
LAG returns a value from a previous row and LEAD from a following row within the partition, based on the ORDER BY. They are used for row-to-row comparisons such as month-over-month change, without a self-join. You can specify an offset and a default value for rows with no neighbour.
What is a window frame?
A frame defines which rows within the partition the function sees for the current row — for example ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW gives a running total, while a fixed range gives a moving average. Frames apply to aggregate window functions and default to a range that surprises people when there are ties.

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

Join CodeBegun and train with working industry engineers — Explore the Java Full Stack 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 16 July 2026 LinkedIn
Chat with us