Q1: ROW_NUMBER vs RANK vs DENSE_RANK with ties

ranking-functions.sql
SELECT name, score,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
  RANK()       OVER (ORDER BY score DESC) AS rk,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dr
FROM results;
-- Bob and Cara are tied at 90
Common guess (all three match)
namescorernrkdr
Bob90222
Cara90322
Dee80432
Actual output
namescorernrkdr
Bob90222
Cara90322
Dee80443

Bob and Cara tie at 90 in all three — the divergence shows up one row later, at Dee. ROW_NUMBER() never repeats a number (4). RANK() repeats 2 for the tie, then skips to 4 for the next row, leaving a gap for the two rows that shared rank 2. DENSE_RANK() also repeats 2, but the next distinct value gets 3, with no gap at all. Full walkthrough with more examples in ROW_NUMBER vs RANK vs DENSE_RANK.

Q2: Window function vs GROUP BY — what actually differs?

GROUP BY (collapses rows)
deptavg_salary
Eng8500
Window function (keeps every row)
namedeptsalaryavg_salary
AnaEng90008500
RaviEng80008500
window-vs-group-by.sql
-- GROUP BY: one row per department
SELECT dept, AVG(salary) AS avg_salary
FROM employees GROUP BY dept;

-- Window function: every employee row, plus the department average
SELECT name, dept, salary,
  AVG(salary) OVER (PARTITION BY dept) AS avg_salary
FROM employees;

This is the core conceptual difference tested in almost every window function question: GROUP BY answers "what's the average per department," discarding individual employees. A window function answers "what's each employee's salary and their department's average," in the same row — which is exactly what's needed to compare an individual to their group.

Q3: PARTITION BY vs GROUP BY

PARTITION BY is not a replacement for GROUP BY — it defines the "window" a function like AVG() or ROW_NUMBER() operates over, without collapsing any rows, as shown directly in Q2. Combine it with ORDER BY inside the same OVER() clause to also control the row-by-row sequence within each partition, which matters for ranking and running-total functions.

Q4: Calculate a running total

running-total.sql
SELECT order_date, total,
  SUM(total) OVER (ORDER BY order_date) AS running_total
FROM orders;
Common guess (grand total repeated)
order_datetotalrunning_total
Jan 150180
Jan 270180
Actual output
order_datetotalrunning_total
Jan 15050
Jan 270120
Jan 360180

Adding ORDER BY inside OVER() changes the default frame from "the whole partition" to "everything from the start up through the current row" — that's precisely what makes it a running total instead of a flat grand total repeated on every row.

Q5: Compare each row to the previous one with LAG

lag-example.sql
SELECT order_date, total,
  total - LAG(total) OVER (ORDER BY order_date) AS change_from_prev
FROM daily_sales;

LAG() reaches back to the previous row's value within the same ordered window, and LEAD() reaches forward — both return NULL at the boundary (the first row has no previous value; the last has no next), which is expected, not an error.

Q6: Why can't WHERE filter a window function directly?

window-in-where-fails.sql
-- Error: window functions are not allowed in WHERE
SELECT name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees
WHERE rn = 1;

-- Correct: wrap it in a subquery or CTE first
SELECT * FROM (
  SELECT name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
  FROM employees
) ranked
WHERE rn = 1;

Window functions are logically computed alongside SELECT, which runs after WHERE in the standard processing order (see SQL Execution Order Explained) — the same reason a SELECT alias isn't visible in WHERE, detailed in SQL Alias Scope Explained. Wrapping the window function in a subquery or CTE makes it a real, already-computed column by the time the outer WHERE runs.

Key takeaways

  • Window functions compute per-row values while keeping every row — GROUP BY collapses rows.
  • RANK() skips numbers after a tie; DENSE_RANK() doesn't; ROW_NUMBER() never repeats at all.
  • PARTITION BY defines a window's scope without removing any rows, unlike GROUP BY.
  • Adding ORDER BY inside OVER() turns a flat aggregate into a running total.
  • A window function must be wrapped in a subquery or CTE before WHERE can filter on its result.