SQL Window Functions Interview Questions
Reviewed by Mark Dickie · Last updated
SQL window functions are functions that perform a calculation across a set of rows related to the current row, without collapsing those rows the way GROUP BY does. For interview prep, the core things to internalize are how PARTITION BY divides rows into groups, how ORDER BY inside the window defines the frame, and the difference between aggregate window functions (SUM, AVG, COUNT) and ranking/value functions (ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG). You should also be comfortable with framing clauses (ROWS BETWEEN ... PRECEDING AND ... FOLLOWING) because interviewers use them to test whether you understand running totals, moving averages, and cumulative sums. Most window-function questions at difficulty 3–5 expect you to produce a single query that ranks or compares rows within partitions, often with a tie-breaking requirement.
| Function | What it returns | Common interview use |
|---|---|---|
ROW_NUMBER() | Sequential integer per row within the partition, no gaps | De-duplication, top-N-per-group |
RANK() | Rank with gaps on ties | Leaderboards where ties share a rank |
DENSE_RANK() | Rank with no gaps on ties | "What place did each finisher get?" |
LEAD(col, n) | Value from the row n positions ahead | Day-over-day change |
LAG(col, n) | Value from the row n positions behind | Comparing to previous period |
SUM(col) OVER(...) | Running or partitioned sum | Cumulative revenue |
What does a SQL window functions interview test?
Interviewers want to see that you can reach for a window function when a GROUP BY would throw away detail you still need. The classic pattern: "Find the second-highest salary per department." A junior candidate writes a self-join or a correlated subquery; a stronger candidate writes ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) and filters for rn = 2. The window function is shorter, easier to read, and less error-prone — but the interviewer is really checking whether you can think in terms of partitioned row sets.
- Know the three clauses:
PARTITION BY(grouping),ORDER BY(sequence within the group), and the frame specification (which rows to include in the calculation). - Practice the top-N-per-group pattern with
ROW_NUMBERorDENSE_RANK— it appears in some form in the majority of window-function interview questions. - Be ready to explain the difference between
RANKandDENSE_RANKwhen there are ties; interviewers ask this directly. - Write running totals and moving averages using
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWand a bounded frame likeROWS BETWEEN 2 PRECEDING AND CURRENT ROW. - Understand that window functions cannot appear in the
WHEREclause — they execute afterWHERE,GROUP BY, andHAVING. Filter on their results using a CTE or subquery.
How do framing clauses change the result?
The frame is the subset of rows the function sees for each calculation. Without an explicit frame, the default depends on the function type: aggregate functions default to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW when an ORDER BY is present, or the whole partition when it is absent. Ranking functions like ROW_NUMBER ignore the frame entirely — they always see the full partition. Getting the frame right is what separates a correct running total from one that silently includes more rows than you intended.
Key facts
- Tarmac has 37 Databases & SQL interview questions on this topic, 25 of them on this page, at difficulty 2–4 of 5.
- Tarmac tracked 8,648 job postings asking for Databases & SQL in August 2026.
- Roles asking for Databases & SQL advertise a median base salary of US$165,000, across 1,586 job postings as of August 2026.
- Tarmac last reviewed these Databases & SQL interview questions on 31 August 2026.
At a glance
| Questions | 25 shown · 37 in the bank |
|---|---|
| Difficulty | 2–4 of 5 |
| Formats | Multiple choice, True / false, Code output, Find the bug, Coding exercise, Short answer |
| Interactive | 4 run your code against tests, in the app |
What you'll review
- window functions
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Databases & SQL/querying/window-functions
Two rows have identical values in the ORDER BY clause of a ROW_NUMBER() OVER (ORDER BY score DESC) window. What does the SQL standard guarantee about their row numbers?#
Options
Show answer
The result is undefined: the engine assigns the tied rows distinct consecutive numbers in an unspecified order. ROW_NUMBER() always produces distinct integers, so no two rows share a number, but when the ORDER BY is not deterministic the standard does not specify which tied row gets the lower number, and that order can vary between runs. Add a tiebreaker column such as ORDER BY score DESC, id ASC for a stable result.
ROW_NUMBER() always produces distinct integers — no two rows ever share a number. When the ORDER BY is not deterministic (ties exist), the standard does not specify which tied row gets the lower number; the engine may assign them in any order, and that order can vary between executions. To get a stable, reproducible result, add a tiebreaker column (e.g. ORDER BY score DESC, id ASC) that makes the ordering fully deterministic.
Databases & SQL/querying/window-functions
You can filter rows using a window function directly in the WHERE clause, e.g. WHERE ROW_NUMBER() OVER (ORDER BY id) <= 5.#
Options
Show answer
False. Window functions are evaluated after WHERE and GROUP BY but before ORDER BY, so they cannot appear in a WHERE clause — the engine hasn't computed them yet at that stage. To filter on a window function result, wrap the query in a subquery or CTE and apply WHERE in the outer query.
Window functions are evaluated after WHERE and GROUP BY but before ORDER BY, so they cannot appear in a WHERE clause — the engine hasn't computed them yet at that stage. To filter on a window function result, wrap the query in a subquery or CTE and apply WHERE in the outer query.
Databases & SQL/querying/window-functions
scores(name, val) has (Ann, 90), (Bo, 90), (Cy, 80). What rn values does this query assign, listed as Ann, Bo, Cy?#
SELECT name, ROW_NUMBER() OVER (ORDER BY val DESC, name ASC) AS rn
FROM scores
ORDER BY rn;Options
Show answer
1, 2, 3
ROW_NUMBER() always assigns a unique, consecutive integer — it never produces ties regardless of the data. The ORDER BY val DESC, name ASC inside the window sorts Ann and Bo (both 90) by name, so Ann gets 1, Bo gets 2, and Cy (80) gets 3. Compare this to RANK(), which would give 1, 1, 3, or DENSE_RANK(), which would give 1, 1, 2.
Databases & SQL/querying/window-functions
The query should number rows independently within each department, but instead numbers all rows sequentially across the whole table. What is wrong?#
SELECT department, employee_name,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS dept_rank
FROM employees;Options
Show answer
PARTITION BY department is missing from the OVER clause, so a single window spans the entire table
ROW_NUMBER() OVER (ORDER BY salary DESC) defines a single window over every row in the result, so numbering is 1, 2, 3 … across all departments. To restart numbering per department, add PARTITION BY department: ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC). Without PARTITION BY, the window function treats the entire result set as one partition.
Databases & SQL/querying/window-functions
Three employees have salaries 100, 100, and 80. When ranked in descending order, what do RANK() and DENSE_RANK() assign to the employee earning 80?#
Options
Show answer
The employee earning 80 gets rank 3 from RANK() and rank 2 from DENSE_RANK(). Both assign rank 1 to the two tied at 100, but RANK() then skips rank 2 because two rows consumed the first two positions, while DENSE_RANK() never skips and increments by one per distinct value. Use DENSE_RANK() for a contiguous sequence, RANK() when the number should reflect how many rows scored higher.
Both functions assign rank 1 to the two employees tied at 100. RANK() then skips rank 2 (because two rows consumed positions 1 and 2) and assigns rank 3 to the 80 employee. DENSE_RANK() never skips — it increments by one for each distinct value — so the 80 employee gets rank 2. Use DENSE_RANK() when you want a contiguous sequence with no gaps; use RANK() when the number assigned should reflect how many rows scored higher.
Databases & SQL/querying/window-functions
A query uses SUM(revenue) OVER (PARTITION BY department_id ORDER BY sale_date). What does omitting PARTITION BY entirely — writing SUM(revenue) OVER (ORDER BY sale_date) — change?#
Options
Show answer
Omitting PARTITION BY makes the window span the entire result set as one partition, so the running sum accumulates across all departments instead of resetting per department. PARTITION BY divides rows into independent groups, resetting the function for each. Without it, SUM(revenue) OVER (ORDER BY sale_date) computes a single running total in date order across every row regardless of department.
PARTITION BY divides the rows into independent groups (like GROUP BY for window functions) — the function resets for each partition. Without it, the entire result set is treated as a single partition. So SUM(revenue) OVER (ORDER BY sale_date) computes a running total across all rows in sale_date order regardless of department, while the original version computes a per-department running total that resets each time the department_id changes.
Databases & SQL/querying/window-functions
RANK() always produces a consecutive sequence of integers with no gaps, even when multiple rows share the same value.#
Options
Show answer
False. RANK() assigns the same rank to tied rows and then skips ahead, producing gaps: two rows tied at rank 1 are both given rank 1, and the next distinct value receives rank 3. DENSE_RANK() is the function that avoids gaps — it also ties equal rows but increments by one for the next distinct value.
RANK() assigns the same rank to tied rows and then skips ahead, producing gaps. For example, two rows tied at rank 1 are both given rank 1, and the next distinct value receives rank 3. DENSE_RANK() is the function that avoids gaps: it also ties equal rows but increments by one for the next distinct value.
Databases & SQL/querying/window-functions
PARTITION BY in a window function resets the running calculation (e.g. ROW_NUMBER(), SUM()) for each new partition group.#
Options
Show answer
True. PARTITION BY divides the result set into independent windows, so ranking, aggregation, and frame boundaries all restart for each partition group — ROW_NUMBER() resets to 1 and SUM() resets to the aggregate's identity. Without PARTITION BY, the entire result set is treated as one window and the calculation runs continuously from the first to the last row.
PARTITION BY divides the result set into independent windows — ranking, aggregation, and frame boundaries all restart at 1 (or the aggregate's identity) for each partition. Without PARTITION BY, the entire result set is treated as one window and numbers run continuously from first to last row.
Databases & SQL/querying/window-functions
results(rep, score) has (Alice, 95), (Bob, 80), (Carol, 80), (Dave, 70). What dr value does each row receive, listed as Alice, Bob, Carol, Dave?#
SELECT rep, DENSE_RANK() OVER (ORDER BY score DESC) AS dr
FROM results
ORDER BY dr, rep;Options
Show answer
1, 2, 2, 3
DENSE_RANK() assigns the same rank to ties but never skips rank values. Alice is uniquely first at 95, so she gets 1. Bob and Carol tie at 80 and both receive 2. Dave, with 70, is the next distinct score and gets 3 — not 4. This is the key difference from RANK(), which would have given Dave rank 4 because two rows occupied positions 2 and 3.
Databases & SQL/querying/window-functions
monthly(month, revenue) has (Jan, 100), (Feb, 120), (Mar, 90) — in that order by month. What does the prev column contain for the Mar row?#
SELECT month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev
FROM monthly;Options
Show answer
120
LAG(revenue) with no offset defaults to offset 1, returning the value from the immediately preceding row in window order. For Mar (the third row), the preceding row is Feb with revenue 120, so prev is 120. The Jan row has no predecessor and receives NULL. LEAD() would look in the opposite direction — returning the next row's value instead.
Databases & SQL/querying/window-functions
The employees table has columns id (INTEGER), name (TEXT), and salary (INTEGER). Write a query ranking employees by salary (highest first) using DENSE_RANK(), so that tied salaries share a rank and the next distinct salary gets the next consecutive rank with no gaps (unlike RANK(), which would skip numbers after a tie). Return columns name, salary, and the rank as salary_rank, ordered by salary_rank ascending, then name ascending.#
Starter code
-- Tied salaries share a rank; no gaps after ties (DENSE_RANK, not RANK).
-- Return: name, salary, salary_rank ordered by salary_rank ASC, name ASC
SELECT
Your solution must pass
- after the tie at rank 2, Dev gets rank 3 (no gap)
This one is written and run, not read. Solve it in the app and your code is executed against these tests and the hidden ones.
Databases & SQL/querying/window-functions
The author wants the top-earning employee per department. The query fails with an error. Which line is the bug?#
SELECT department, employee_name, salary
FROM employees
WHERE RANK() OVER (PARTITION BY department ORDER BY salary DESC) = 1;Show answer
The bug is on line 3.
Window functions are evaluated after WHERE, so they cannot appear in a WHERE clause — the database raises an error (e.g. "window functions are not allowed in WHERE"). The fix is to wrap the window function in a CTE or subquery and filter in the outer query:
WITH ranked AS (
SELECT department, employee_name, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT department, employee_name, salary
FROM ranked
WHERE rnk = 1;
Databases & SQL/querying/window-functions
Both GROUP BY and a window function like AVG(salary) OVER (...) can compute an average per group. What is the key difference in what each returns?#
Show answer
GROUP BY collapses each group down to a single output row, so you lose the individual rows and can only emit aggregates and the grouping columns. A window function computes the aggregate over a partition but keeps every input row, attaching the group-level value to each detail row alongside its own columns. So if you want each employee's row to also show their department average, you use a window function; GROUP BY would give you one row per department instead.
GROUP BY reduces N rows in a group to one summary row, discarding the detail. A window function (OVER (...)) evaluates the same aggregate across a partition but does not collapse rows — every original row survives and gets the computed value appended. That row-preserving behaviour is exactly why window functions are reached for when you need both the detail and a group-level metric (average, total, rank) in the same result set.
Databases & SQL/querying/window-functions
In a window function, what does the PARTITION BY clause do, and how does it differ from ORDER BY inside the same OVER (...)?#
Show answer
PARTITION BY splits the rows into independent groups and the window function is computed separately within each group; the calculation resets at every partition boundary and never spans partitions. ORDER BY inside OVER (...) instead defines the row ordering within a partition, which is what gives meaning to running/cumulative aggregates and to ranking functions. Omitting PARTITION BY treats the whole result set as one partition.
PARTITION BY is the windowing analogue of GROUP BY: it divides rows into partitions and the function restarts for each one, so an aggregate or rank is scoped to its partition. ORDER BY within the OVER clause sequences rows inside a partition — it controls the order a running total accumulates in or the order a RANK assigns ranks in, but it does not itself create groups. With no PARTITION BY, the entire input is a single partition.
Databases & SQL/querying/window-functions
Three rows tie for the top score. How do ROW_NUMBER(), RANK(), and DENSE_RANK() each number those rows and the row that follows them?#
Show answer
ROW_NUMBER() ignores ties and assigns 1, 2, 3 arbitrarily among the tied rows (the order is non-deterministic unless ORDER BY breaks the tie). RANK() gives all three tied rows the same rank 1 and then skips ahead, so the next row is rank 4 — there is a gap. DENSE_RANK() also gives the tied rows rank 1 but does not skip, so the next row is rank 2 — no gap.
All three are ranking window functions over an ordered partition but differ on ties. ROW_NUMBER() always produces a unique sequential number, breaking ties arbitrarily. RANK() assigns equal rank to ties and then leaves a gap (1,1,1,4). DENSE_RANK() assigns equal rank to ties with no gap (1,1,1,2). Choosing wrongly is a classic top-N-per-group bug: ROW_NUMBER() returns exactly one row per group, while RANK()/DENSE_RANK() <= N returns all rows tied at the cutoff.
Databases & SQL/querying/window-functions
How does SUM(amount) OVER (PARTITION BY region) differ from SUM(amount) ... GROUP BY region?#
Options
Show answer
The window (OVER) form returns each region's total alongside every original row, whereas GROUP BY collapses each region into a single summary row. A window function computes the aggregate over a partition but attaches the result to each input row instead of collapsing them, so you get every order plus its region total in one pass without a self-join. That row-preserving behaviour is the whole point of OVER (...).
A window function computes an aggregate over a partition but attaches the result to each input row rather than collapsing them — so you get every order row plus its region's total in one pass, without a self-join. GROUP BY reduces each region to a single summary row, losing the individual rows. That row-preserving behaviour is the whole point of OVER (...).
Databases & SQL/querying/window-functions
A window is defined as OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). Two rows share the same order_date. How does changing ROWS to RANGE affect which rows each of those two rows includes in its frame?#
Options
Show answer
With ROWS, each row's frame ends exactly at that physical row; with RANGE, both rows' frames extend to include all rows sharing the same order_date value, so the two tied rows include each other. ROWS defines the frame by physical offsets, while RANGE treats equal ORDER BY values as one peer group. For running totals this matters: ROWS gives a strict cumulative sum, whereas RANGE can double-count same-date rows.
ROWS mode defines the frame in terms of physical row offsets — CURRENT ROW means exactly this row, so each tied row has a different frame boundary. RANGE mode treats all rows with the same ORDER BY value as peers; CURRENT ROW in RANGE mode means all rows in the same peer group. As a result, both rows sharing order_date include each other in their frames when using RANGE, which can produce different aggregated values than ROWS. This distinction is especially important for running totals: ROWS gives a strict cumulative sum; RANGE can double-count the same-date rows.
Databases & SQL/querying/window-functions
When ORDER BY is present in a window function but no explicit frame clause is written, the default frame is ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING (the whole partition).#
Options
Show answer
False. With ORDER BY present but no explicit frame, SQL defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — a cumulative frame up to and including all rows tying with the current row, not the whole partition. The whole-partition frame applies only when there is no ORDER BY at all. This matters for running totals: a cumulative SUM with ORDER BY differs sharply from a partition-wide SUM.
When ORDER BY is specified without an explicit frame clause, SQL defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — a cumulative frame up to and including all rows that tie with the current row. The whole-partition frame (UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) is only used when there is no ORDER BY at all. This distinction matters for running totals: a cumulative SUM with ORDER BY behaves very differently from a partition-wide SUM.
Databases & SQL/querying/window-functions
employees(name, score) has (Ann, 90), (Bo, 90), (Cy, 80). What r value does each row get, listed as Ann, Bo, Cy?#
SELECT name, RANK() OVER (ORDER BY score DESC) AS r
FROM employees
ORDER BY r, name;Options
Show answer
1, 1, 3
RANK() gives tied rows the same rank and then skips the next ranks. Ann and Bo both score 90, so both rank 1; Cy scores 80 and gets rank 3 (not 2), because two rows already occupied positions 1 and 2. DENSE_RANK() would give Cy 2 (no gap), and ROW_NUMBER() would give 1, 2, 3 (no ties). The gap after a tie is exactly what distinguishes RANK from DENSE_RANK.
Databases & SQL/querying/window-functions
The products table has columns id (INTEGER), name (TEXT), category (TEXT), and revenue (INTEGER). Write a query returning the top 3 products by revenue within each category, using ROW_NUMBER(). When two products in a category tie on revenue, rank the alphabetically earlier name higher. Return columns category, name, revenue, ordered by category ascending, then revenue descending, then name ascending.#
Starter code
-- Rank products per category with ROW_NUMBER(), keep the top 3.
-- Return: category, name, revenue ordered by category ASC, revenue DESC, name ASC
SELECT
Your solution must pass
- Cable misses Electronics' top 3; both Home products qualify
This one is written and run, not read. Solve it in the app and your code is executed against these tests and the hidden ones.
Databases & SQL/querying/window-functions
The contacts table has columns id (INTEGER), email (TEXT), and updated_at (TEXT, ISO date YYYY-MM-DD). The table contains duplicate rows per email. Write a query that returns only the latest row for each email — the one with the greatest updated_at, breaking ties by the greatest id — using ROW_NUMBER(). Return columns id, email, updated_at, ordered by email ascending.#
Starter code
-- Number each email's rows newest-first with ROW_NUMBER(), keep row 1.
-- Return: id, email, updated_at ordered by email ASC
SELECT
Your solution must pass
- keeps the newest row per email
This one is written and run, not read. Solve it in the app and your code is executed against these tests and the hidden ones.
Databases & SQL/querying/window-functions
The monthly_sales table has columns month (TEXT, YYYY-MM, one row per month) and total (INTEGER). Write a query computing each month's change versus the previous month using LAG(). Return columns month, total, and delta (this month's total minus the previous month's total; NULL for the first month, since it has no predecessor), ordered by month ascending.#
Starter code
-- LAG(total) OVER (...) fetches the previous month's total.
-- Return: month, total, delta ordered by month ASC
SELECT
Your solution must pass
- deltas track rises and falls; the first month is NULL
This one is written and run, not read. Solve it in the app and your code is executed against these tests and the hidden ones.
Databases & SQL/querying/window-functions
The query should compute a running total of amount ordered by txn_date, but every row shows the grand total instead. Which line contains the bug?#
SELECT txn_date, amount,
SUM(amount) OVER (
PARTITION BY account_id
ORDER BY txn_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS running_total
FROM transactions;Show answer
The bug is on line 5.
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING extends the frame to every row in the partition, so SUM always returns the partition total — not a running total. A running total requires the frame to end at the current row: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. That is also the default frame when ORDER BY is present, so removing the explicit ROWS clause entirely fixes the query.
Databases & SQL/querying/window-functions
The developer wants the total sales per region alongside each sale's percentage of that region's total. The query errors on execution. What is the bug?#
SELECT region, sale_id, amount,
SUM(amount) OVER (PARTITION BY region) AS region_total,
amount / SUM(amount) OVER (PARTITION BY region) * 100 AS pct
FROM sales
GROUP BY region;Options
Show answer
GROUP BY region collapses rows before the window function can see individual sale_id and amount values, making the window function invalid
GROUP BY region reduces the result to one row per region before window functions run. At that point sale_id and amount are no longer meaningful individual values, so the database rejects them in the SELECT list (they are neither in the GROUP BY nor wrapped in an aggregate). Remove the GROUP BY entirely — window functions compute aggregates across rows without collapsing them, so SUM(amount) OVER (PARTITION BY region) already gives each row the region total while keeping every sale row intact.
Databases & SQL/querying/window-functions
Within a window's frame clause, what is the difference between ROWS BETWEEN ... and RANGE BETWEEN ..., and when does it actually matter?#
Show answer
ROWS defines the frame by physical row position — e.g. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW is exactly the current row and the two physical rows before it. RANGE defines it by the ORDER BY value, so it includes every row whose ordering value falls in the logical range, which means all peer rows that tie on the ORDER BY value are pulled into the frame together. The difference bites when there are duplicate ORDER BY values: with RANGE a running total jumps to include all tied rows at once, whereas ROWS advances one physical row at a time.
A frame clause bounds which rows in the partition feed the aggregate. ROWS counts physical rows offset from the current row; RANGE works on the ORDER BY value and treats peers (rows with equal ordering values) as a single unit, including them all or none. They behave identically when the ORDER BY key is unique, so the distinction only surfaces with ties — where RANGE gives the same frame to every tied row and ROWS does not. The implicit default frame when you specify ORDER BY but no frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, a common source of surprise.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See databases-sql salaries and hiring demand from live job postings.
The other 12 questions
This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 12 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan