SQLab Hub
Lesson 29 · Window Functions
Lesson 29 · Advanced

SQL Window Functions: OVER, PARTITION BY, RANK and LAG

A window function calculates a value from a group of rows, but unlike GROUP BY it keeps every row in the result. That is how you put a department's average next to each employee, number rows, rank them, or add a running total, all without losing the detail rows.

OVER · PARTITION BY · ORDER BY Ranking, running totals, LAG / LEAD ▶ Runnable Playground examples 🧪 8 practice exercises 📝 41-question quiz ⏱ ~40 min

What Is a Window Function?

GROUP BY answers "what is the average salary per department?" with one row per department. A window function answers the same question but writes the answer on every employee's row, so you can compare each person with their department:

SQLite · runs in the Playground
SELECT first_name, department, salary,
       ROUND(AVG(salary) OVER (PARTITION BY department)) AS dept_avg,
       salary - ROUND(AVG(salary) OVER (PARTITION BY department)) AS difference
FROM employees
ORDER BY department, salary DESC
LIMIT 8;

▶ Run this query in the SQL Playground

Each employee next to their department average
first_namedepartmentsalarydept_avgdifference
NancyCustomer Success20399913414069859
MariaCustomer Success19112013414056980
BarbaraCustomer Success18828013414054140
MelissaCustomer Success17839113414044251
DorothyCustomer Success16511813414030978
CharlesCustomer Success16290113414028761
SarahCustomer Success15215413414018014
EdwardCustomer Success1377371341403597
✓ 8 rows · real output from the Playground database

OVER (PARTITION BY department) is what makes AVG a window function. "Window" means the set of rows the function looks at for the current row; here that is all rows of the same department. All 120 employees stay in the result.

GROUP BYWindow function
Rows in the resultOne per groupEvery input row
Detail columnsOnly grouped columns and aggregatesAny column
Typical questionTotal per monthRunning total, rank, previous value, share of total
Written asSUM(x) ... GROUP BY gSUM(x) OVER (PARTITION BY g)

The OVER Clause

SQL · syntax
function_name(arguments) OVER (
  [PARTITION BY column, ...]   -- split rows into groups
  [ORDER BY column, ...]       -- order rows inside each group
  [ROWS BETWEEN ... AND ...]   -- which rows around the current one to use
)
PartWhat it doesIf you leave it out
PARTITION BYThe calculation restarts for each groupThe whole result is one group
ORDER BYDefines "previous", "next" and the order for ranking and running totalsRows have no order inside the window
Frame (ROWS / RANGE)Limits the rows used, such as "the last 3 rows"With ORDER BY: from the start up to the current row. Without: the whole partition

An empty OVER () uses all rows. That makes "share of the total" a one-liner:

SQLite · runs in the Playground
SELECT category,
       ROUND(SUM(price), 2) AS list_value,
       ROUND(100.0 * SUM(price) / SUM(SUM(price)) OVER (), 1) AS pct_of_total
FROM products
GROUP BY category
ORDER BY pct_of_total DESC;

▶ Run this query in the SQL Playground

Each category's share of the catalogue's list value
categorylist_valuepct_of_total
Food & Beverage21240.8616.9
Office Supplies16043.5712.8
Books15926.9812.7
Automotive12771.9910.2
Sports & Outdoors12780.2510.2
Home & Garden12426.839.9
Toys11280.239
Clothing10825.618.6
Health & Beauty7219.455.7
Electronics5268.74.2
✓ 10 rows · real output from the Playground database

SUM(SUM(price)) OVER () looks odd but is common: the inner SUM is the GROUP BY total per category, the outer SUM adds those totals across all categories. Window functions are calculated after GROUP BY, so they can use its results.

Ranking: ROW_NUMBER, RANK, DENSE_RANK and NTILE

The three ranking functions differ only in how they handle ties. Product ratings have plenty of ties, so they show the difference well:

SQLite · runs in the Playground
SELECT name, rating,
       ROW_NUMBER() OVER (ORDER BY rating DESC, name) AS row_num,
       RANK()       OVER (ORDER BY rating DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY rating DESC) AS dense_rnk
FROM products
ORDER BY rating DESC, name
LIMIT 10;

▶ Run this query in the SQL Playground

ROW_NUMBER, RANK and DENSE_RANK on tied ratings
nameratingrow_numrnkdense_rnk
Japanese Green Tea 50 bags5111
Laptop Pro 15"5211
Play Kitchen Deluxe5311
Premium Beard Oil 60ml5411
SPF 50 Mineral Sunscreen5511
The Manager's Path5611
Artisan Hot Sauce Trio4.9772
The Manager's Path4.9872
The Pragmatic Programmer4.9972
Zero to One (Thiel)4.91072
✓ 10 rows · real output from the Playground database
FunctionTiesAfter a tie of six at the top
ROW_NUMBER()Different numbers (order among ties needs a tie-breaker)1, 2, 3, 4, 5, 6, 7 ...
RANK()Same number, then a gap1, 1, 1, 1, 1, 1, 7 ...
DENSE_RANK()Same number, no gap1, 1, 1, 1, 1, 1, 2 ...

Add PARTITION BY to rank within groups. Each department's salaries, ranked from the top:

SQLite · runs in the Playground
SELECT department, first_name, salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees
WHERE department IN ('Design', 'Product')
ORDER BY department, rank_in_dept;

▶ Run this query in the SQL Playground

Salary rank within each department
departmentfirst_namesalaryrank_in_dept
DesignAmy1945431
DesignJoshua1916572
DesignSandra1870893
DesignAshley1717554
DesignJason1568655
DesignMaria1444816
DesignMichael1184657
DesignAshley751208
DesignRobert536309
DesignPaul4604410
✓ 19 rows · first 10 shown · real output from the Playground database

NTILE: equal-sized buckets

NTILE(n) splits the ordered rows into n groups of (nearly) equal size, for quartiles, deciles and similar bands:

SQLite · runs in the Playground
SELECT quartile,
       COUNT(*) AS employees,
       MIN(salary) AS lowest,
       MAX(salary) AS highest
FROM (SELECT salary, NTILE(4) OVER (ORDER BY salary DESC) AS quartile
      FROM employees) t
GROUP BY quartile
ORDER BY quartile;

▶ Run this query in the SQL Playground

Employees split into four salary quartiles
quartileemployeeslowesthighest
130167158204449
230121887166288
33084262121695
4304604484033
✓ 4 rows · real output from the Playground database

Each ranking function has its own lesson with more examples: ROW_NUMBER and RANK and DENSE_RANK.

Running Totals and Moving Averages

An aggregate with ORDER BY in its OVER clause becomes cumulative: each row sums everything up to and including itself. Here is revenue per month with a running total that restarts each year:

SQLite · runs in the Playground
WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         ROUND(SUM(total), 2) AS revenue
  FROM orders
  WHERE order_date >= '2023-01-01'
  GROUP BY month
)
SELECT month, revenue,
       ROUND(SUM(revenue) OVER (PARTITION BY substr(month, 1, 4)
                                ORDER BY month), 2) AS year_to_date
FROM monthly
ORDER BY month
LIMIT 8;

▶ Run this query in the SQL Playground

Monthly revenue with a year-to-date running total
monthrevenueyear_to_date
2023-0110518.8210518.82
2023-0210464.520983.32
2023-0323221.3544204.67
2023-047355.1551559.82
2023-0526116.8377676.65
2023-0611301.8688978.51
2023-078479.4397457.94
2023-0819166.49116624.43
✓ 8 rows · real output from the Playground database

Moving average with a window frame

A frame limits the window to rows near the current one. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW means "this month and the two before it", a three-month moving average:

SQLite · runs in the Playground
WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         ROUND(SUM(total), 2) AS revenue
  FROM orders
  WHERE order_date >= '2023-01-01'
  GROUP BY month
)
SELECT month, revenue,
       ROUND(AVG(revenue) OVER (ORDER BY month
                                ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS avg_3_months
FROM monthly
ORDER BY month
LIMIT 8;

▶ Run this query in the SQL Playground

Three-month moving average of revenue
monthrevenueavg_3_months
2023-0110518.8210518.82
2023-0210464.510491.66
2023-0323221.3514734.89
2023-047355.1513680.33
2023-0526116.8318897.78
2023-0611301.8614924.61
2023-078479.4315299.37
2023-0819166.4912982.59
✓ 8 rows · real output from the Playground database

The first two months average fewer than three values because there are not enough earlier rows yet.

FrameRows used
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWFrom the first row to this one (a running total)
ROWS BETWEEN 2 PRECEDING AND CURRENT ROWThis row and the two before it
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWINGThe previous, current and next row
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGThis row to the last one
ROWS or RANGE? ROWS counts physical rows. RANGE groups rows that have the same ORDER BY value, so tied rows all get the same running total. The default frame when you write ORDER BY is RANGE, which is why a running total over a column with repeated values can jump in steps. Write ROWS explicitly when you want one row at a time.

LAG, LEAD, FIRST_VALUE and LAST_VALUE

LAG reads a value from an earlier row and LEAD from a later one. With LAG(revenue, 12) on monthly data you compare each month with the same month a year earlier:

SQLite · runs in the Playground
WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         ROUND(SUM(total), 2) AS revenue
  FROM orders
  WHERE order_date >= '2023-01-01'
  GROUP BY month
)
SELECT month, revenue,
       LAG(revenue)     OVER (ORDER BY month) AS previous_month,
       LAG(revenue, 12) OVER (ORDER BY month) AS same_month_last_year,
       ROUND(100.0 * (revenue - LAG(revenue, 12) OVER (ORDER BY month))
             / LAG(revenue, 12) OVER (ORDER BY month), 1) AS yoy_pct
FROM monthly
ORDER BY month DESC
LIMIT 6;

▶ Run this query in the SQL Playground

Revenue compared with the previous month and the same month a year earlier
monthrevenueprevious_monthsame_month_last_yearyoy_pct
2024-1210621.3617380.7229761.96-64.3
2024-1117380.727996.729310.4786.7
2024-107996.724319.4612822.83-37.6
2024-094319.4625429.0916095.42-73.2
2024-0825429.0910296.6719166.4932.7
2024-0710296.675186.498479.4321.4
✓ 6 rows · real output from the Playground database

The LAG and LEAD lesson covers offsets, defaults and gaps between a customer's orders.

First and last value in a group

FIRST_VALUE returns the first value in the window. LAST_VALUE needs care: with ORDER BY the default frame ends at the current row, so it returns the current row unless you extend the frame to the end:

SQLite · runs in the Playground
SELECT user_id, order_date, total,
       FIRST_VALUE(order_date) OVER (PARTITION BY user_id ORDER BY order_date, id) AS first_order,
       LAST_VALUE(order_date)  OVER (PARTITION BY user_id ORDER BY order_date, id
                                     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order
FROM orders
WHERE user_id IN (4, 77)
ORDER BY user_id, order_date;

▶ Run this query in the SQL Playground

Each order with the customer's first and last order date
user_idorder_datetotalfirst_orderlast_order
42022-08-222501.232022-08-222024-11-15
42024-04-20843.72022-08-222024-11-15
42024-11-151518.362022-08-222024-11-15
772022-12-154595.722022-12-152024-08-16
772023-03-184770.012022-12-152024-08-16
772023-06-03345.482022-12-152024-08-16
772024-01-111613.312022-12-152024-08-16
772024-08-163268.652022-12-152024-08-16
✓ 8 rows · real output from the Playground database

Common Window Function Patterns

Top N per group

Window functions cannot be used in WHERE, because WHERE runs before they are calculated. Number the rows in a subquery or CTE, then filter outside. The three newest orders per status:

SQLite · runs in the Playground
SELECT status, id, order_date, total
FROM (SELECT status, id, order_date, total,
             ROW_NUMBER() OVER (PARTITION BY status ORDER BY order_date DESC, id DESC) AS rn
      FROM orders) t
WHERE rn <= 3
ORDER BY status, rn;

▶ Run this query in the SQL Playground

Three newest orders for each status
statusidorder_datetotal
cancelled1552024-12-19110.6
cancelled222024-08-121691.82
cancelled1442024-06-283701.09
delivered1712024-12-232162.1
delivered1692024-12-121308.16
delivered972024-12-094566.23
pending702024-12-242104.22
pending1682024-11-014271.87
pending1122024-07-06441.29
processing112024-11-151518.36
✓ 18 rows · first 10 shown · real output from the Playground database

Finding duplicates

Number the rows within each group of identical values; every row numbered 2 or more is a duplicate:

SQLite · runs in the Playground
SELECT id, name, rn
FROM (SELECT id, name,
             ROW_NUMBER() OVER (PARTITION BY name ORDER BY id) AS rn
      FROM products) t
WHERE rn > 1
ORDER BY name, rn
LIMIT 8;

▶ Run this query in the SQL Playground

Repeated product names after the first one
idnamern
1022000A Jump Starter Pack2
464K Dual Dash Camera2
724K Dual Dash Camera3
50A5 Hardcover Notebook 3pk2
24Artisan Hot Sauce Trio2
40Artisan Hot Sauce Trio3
67Artisan Hot Sauce Trio4
75Artisan Hot Sauce Trio5
✓ 8 rows · real output from the Playground database

Several windows in one query

Each window function has its own OVER clause, so different partitions can sit side by side:

SQLite · runs in the Playground
SELECT first_name, department, salary,
       RANK() OVER (ORDER BY salary DESC) AS company_rank,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
       COUNT(*) OVER (PARTITION BY department) AS dept_size
FROM employees
ORDER BY company_rank
LIMIT 6;

▶ Run this query in the SQL Playground

Company rank, department rank and department size together
first_namedepartmentsalarycompany_rankdept_rankdept_size
StevenMarketing2044491113
NancyCustomer Success2039992115
JohnOperations2034673113
DonnaEngineering1990294111
MatthewMarketing1965305213
PaulFinance1960326110
✓ 6 rows · real output from the Playground database

When Window Functions Are Calculated

StepClause
1FROM and JOIN
2WHERE
3GROUP BY
4HAVING
5Window functions (in SELECT)
6DISTINCT
7ORDER BY
8LIMIT

Two consequences follow. A window function cannot appear in WHERE, GROUP BY or HAVING; wrap the query and filter outside. And WHERE removes rows before the window is calculated, so filtering first changes the result: a LAG on a query filtered to one month has no previous row to read.

Window Functions in Each Database

DatabaseSupport
PostgreSQLFull, since 8.4. Also FILTER, GROUPS frames and named windows (WINDOW w AS ...)
SQL ServerRanking since 2005; LAG, LEAD, FIRST_VALUE and frames since 2012
OracleFull (it calls them analytic functions)
MySQLFrom 8.0 only; MySQL 5.7 has none
SQLiteFrom 3.25 (the Playground runs 3.49)

Window Functions and Performance

Most window functions need the rows sorted by the PARTITION BY and ORDER BY columns. That sort is usually the expensive part.

TipWhy
Filter with WHERE in an inner query first, when the logic allows itFewer rows to sort
An index on (partition columns, order columns) can remove the sortThe rows can be read in order
Reuse the same OVER clause where possibleWindows with identical definitions can share one sort
Prefer one window function to a correlated subquery on large tablesOne pass instead of one lookup per row

Common Window Function Mistakes

Six errors and how to avoid them.

1. Using a window function in WHERE

❌ Error
SQL
SELECT *
FROM employees
WHERE ROW_NUMBER() OVER (ORDER BY salary DESC) <= 3;
✅ Filter in an outer query
SQL
SELECT *
FROM (SELECT e.*, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
      FROM employees e) t
WHERE rn <= 3;

WHERE is evaluated before window functions exist.

2. ROW_NUMBER without a deterministic ORDER BY

❌ Ties numbered in random order
SQL
ROW_NUMBER() OVER (ORDER BY rating DESC)
✅ Add a tie-breaker
SQL
ROW_NUMBER() OVER (ORDER BY rating DESC, id)

Six products share the top rating. Without a tie-breaker their numbers can change from one run to the next.

3. LAST_VALUE with the default frame

❌ Returns the current row
SQL
LAST_VALUE(order_date) OVER (PARTITION BY user_id ORDER BY order_date)
✅ Frame to the end of the partition
SQL
LAST_VALUE(order_date) OVER (PARTITION BY user_id ORDER BY order_date
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)

With ORDER BY, the default frame stops at the current row, so the "last" value is the current one.

4. Forgetting PARTITION BY

❌ One ranking for the whole company
SQL
RANK() OVER (ORDER BY salary DESC)
✅ Ranking per department
SQL
RANK() OVER (PARTITION BY department ORDER BY salary DESC)

Without PARTITION BY the window is all rows, so the numbering never restarts.

5. Expecting GROUP BY behaviour

❌ 120 rows, not 10
SQL
SELECT department, AVG(salary) OVER (PARTITION BY department)
FROM employees;
✅ GROUP BY for one row per group
SQL
SELECT department, AVG(salary)
FROM employees
GROUP BY department;

A window function keeps every row. If you want one row per group, use GROUP BY (or add DISTINCT).

6. Filtering away the rows LAG needs

❌ previous is NULL
SQL
SELECT month, LAG(revenue) OVER (ORDER BY month) AS previous
FROM monthly
WHERE month = '2024-12';
✅ Calculate first, filter after
SQL
SELECT * FROM (
  SELECT month, LAG(revenue) OVER (ORDER BY month) AS previous
  FROM monthly) t
WHERE month = '2024-12';

WHERE runs before the window, so November is gone before LAG looks for it.

Window Functions Cheat Sheet

TaskPattern
Group value on every rowAVG(salary) OVER (PARTITION BY department)
Share of totalx / SUM(x) OVER ()
Number rowsROW_NUMBER() OVER (ORDER BY x, id)
Rank with tiesRANK() or DENSE_RANK() OVER (ORDER BY x DESC)
BucketsNTILE(4) OVER (ORDER BY x)
Running totalSUM(x) OVER (ORDER BY d ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
Moving averageAVG(x) OVER (ORDER BY d ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
Previous / nextLAG(x) OVER (ORDER BY d), LEAD(x) OVER (ORDER BY d)
Top N per groupROW_NUMBER() OVER (PARTITION BY g ORDER BY x DESC), then WHERE rn <= N outside

Practice Window Functions Queries

8 exercises on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database.

  1. 1Easy Show each product with the average price of its category next to it. Show 8, ordered by category and price. Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT name, category, price,
           ROUND(AVG(price) OVER (PARTITION BY category), 2) AS category_avg
    FROM products
    ORDER BY category, price DESC
    LIMIT 8;

    ▶ Run this query in the SQL Playground

    PARTITION BY category computes one average per category and repeats it on each row.

  2. 2Easy Number the employees from the most recently hired (1) to the earliest. Show the first 5. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT ROW_NUMBER() OVER (ORDER BY hire_date DESC, id) AS n,
           first_name, last_name, hire_date
    FROM employees
    ORDER BY n
    LIMIT 5;

    ▶ Run this query in the SQL Playground

    id breaks ties between employees hired on the same day.

  3. 3Medium For each customer's orders, show a running total of what they have spent so far. Use customers 4 and 77. Table: orders

    Show solution
    SQLite · runs in the Playground
    SELECT user_id, order_date, total,
           ROUND(SUM(total) OVER (PARTITION BY user_id ORDER BY order_date, id
                                  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) AS spent_so_far
    FROM orders
    WHERE user_id IN (4, 77)
    ORDER BY user_id, order_date;

    ▶ Run this query in the SQL Playground

    The running total restarts for each customer because of PARTITION BY user_id.

  4. 4Medium Show the cheapest product in each category. Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT category, name, price
    FROM (SELECT category, name, price,
                 ROW_NUMBER() OVER (PARTITION BY category ORDER BY price, id) AS rn
          FROM products) t
    WHERE rn = 1
    ORDER BY price;

    ▶ Run this query in the SQL Playground

    Number within each category from the lowest price and keep number 1.

  5. 5Medium Show each rating value with its number of reviews and its share of all reviews in percent. Table: reviews

    Show solution
    SQLite · runs in the Playground
    SELECT rating,
           COUNT(*) AS reviews,
           ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
    FROM reviews
    GROUP BY rating
    ORDER BY rating DESC;

    ▶ Run this query in the SQL Playground

    SUM(COUNT(*)) OVER () adds up the grouped counts across all ratings.

  6. 6Medium Show each Design employee with the salary just below and just above theirs in the same department. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT first_name, salary,
           LAG(salary) OVER (PARTITION BY department ORDER BY salary) AS next_lower,
           LEAD(salary) OVER (PARTITION BY department ORDER BY salary) AS next_higher
    FROM employees
    WHERE department = 'Design'
    ORDER BY salary;

    ▶ Run this query in the SQL Playground

    Ordered by salary, LEAD looks one step up and LAG one step down.

  7. 7Hard Find the month with the highest revenue in each year. Table: orders

    Show solution
    SQLite · runs in the Playground
    WITH monthly AS (
      SELECT strftime('%Y', order_date) AS yr,
             strftime('%Y-%m', order_date) AS month,
             ROUND(SUM(total), 2) AS revenue
      FROM orders
      GROUP BY yr, month
    )
    SELECT yr, month, revenue
    FROM (SELECT *, RANK() OVER (PARTITION BY yr ORDER BY revenue DESC) AS rnk
          FROM monthly) t
    WHERE rnk = 1
    ORDER BY yr;

    ▶ Run this query in the SQL Playground

    Group to months first, then rank the months within each year.

  8. 8Hard Split customers who have ordered into four groups by total spend (1 = highest) and show each group's size and spend range. Table: users, orders

    Show solution
    SQLite · runs in the Playground
    SELECT spend_quartile,
           COUNT(*) AS customers,
           MIN(spent) AS lowest,
           MAX(spent) AS highest
    FROM (SELECT user_id, ROUND(SUM(total), 2) AS spent,
                 NTILE(4) OVER (ORDER BY SUM(total) DESC) AS spend_quartile
          FROM orders
          GROUP BY user_id) t
    GROUP BY spend_quartile
    ORDER BY spend_quartile;

    ▶ Run this query in the SQL Playground

    NTILE runs after GROUP BY, so it can order by the grouped total.

📝 Quiz — Window Functions

Test your understanding · 0/41 answered

Question 1 of 41

What is the defining feature of a window function?

Question 2 of 41

What does the OVER clause do?

Question 3 of 41

What is the main difference between GROUP BY and window functions?

Question 4 of 41

What does ROW_NUMBER() OVER (ORDER BY salary DESC) produce?

Question 5 of 41

How does RANK() treat ties?

Question 6 of 41

What is the difference between RANK and DENSE_RANK?

Question 7 of 41

What does PARTITION BY do?

Question 8 of 41

What does SUM(total) OVER (ORDER BY order_date) compute?

Question 9 of 41

How is a moving average built?

Question 10 of 41

What does LAG(revenue) OVER (ORDER BY month) return?

Question 11 of 41

What does LEAD(revenue) OVER (ORDER BY month) return?

Question 12 of 41

What does FIRST_VALUE(total) OVER (PARTITION BY user_id ORDER BY order_date) return?

Question 13 of 41

What does LAST_VALUE usually need to work as expected?

Question 14 of 41

What does NTILE(4) OVER (ORDER BY salary DESC) do?

Question 15 of 41

What is a window frame?

Question 16 of 41

What does ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW mean?

Question 17 of 41

How do you get the top 3 products per category?

Question 18 of 41

How can a window function find duplicates?

Question 19 of 41

What is month-over-month growth usually built on?

Question 20 of 41

Can a window function be used directly in WHERE?

Question 21 of 41

Which is a typical window function interview question?

Question 22 of 41

How do you compute a rolling 7-day average of daily sales?

Question 23 of 41

Why do analysts use window functions so much?

Question 24 of 41

What does COUNT(*) OVER () return?

Question 25 of 41

Do all window functions need ORDER BY inside OVER?

Question 26 of 41

What is a common ROW_NUMBER mistake?

Question 27 of 41

What is year-over-year growth on monthly data usually built with?

Question 28 of 41

How can you show each customer's first and last purchase date in one query?

Question 29 of 41

What is cohort analysis, and how do window functions help?

Question 30 of 41

What can EXPLAIN show about a query with ROW_NUMBER() OVER (ORDER BY salary DESC)?

Question 31 of 41

Are window functions standard SQL?

Question 32 of 41

How does PARTITION BY differ from GROUP BY?

Question 33 of 41

How does RANGE BETWEEN differ from ROWS BETWEEN?

Question 34 of 41

How do you show each row's percentage of the total?

Question 35 of 41

What does ORDER BY inside OVER do for a running total?

Question 36 of 41

How do you rank employees within each department by salary?

Question 37 of 41

How do window functions help with funnel analysis?

Question 38 of 41

Which function splits customers into 10 equal groups by value?

Question 39 of 41

Can two window functions in one SELECT use different PARTITION BY clauses?

Question 40 of 41

How could you find employees whose salary has not changed in two years, given a salary history table?

Question 41 of 41

How important are window functions in interviews?

Window Functions Interview Questions

Q1. What is the difference between a window function and GROUP BY?

GROUP BY collapses each group into one row. A window function calculates over a group of rows but keeps every row, so detail and group values can appear together.

Q2. ROW_NUMBER, RANK or DENSE_RANK?

ROW_NUMBER for exactly one row per position, for example "the latest order per customer". RANK when ties should share a place and leave a gap, as in sports results. DENSE_RANK when ties share a place without gaps, as in "the Nth highest distinct salary".

Q3. Why can a window function not be used in WHERE?

WHERE is evaluated before the SELECT list, where window functions are computed. Compute the value in a subquery or CTE and filter in the outer query.

Q4. What is the default window frame?

With ORDER BY: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Without ORDER BY: the whole partition. This is why LAST_VALUE needs an explicit frame.

Q5. How do you calculate a running total that restarts each year?

SUM(revenue) OVER (PARTITION BY year ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).

Q6. How do you get the top 3 per group?

Number the rows with ROW_NUMBER() OVER (PARTITION BY group ORDER BY value DESC) in a subquery or CTE and keep rn <= 3. Use RANK instead if ties should all be included.

Q7. How do you compute month-over-month change?

Aggregate to one row per month, then revenue - LAG(revenue) OVER (ORDER BY month), and divide by the LAG value for a percentage. The first month has no previous value and gives NULL.

Frequently Asked Questions

What are window functions in SQL?

Window functions calculate a value across a set of related rows while still returning one row per input row. Unlike GROUP BY, they never collapse the result.

What is the OVER clause?

OVER turns an ordinary function into a window function and defines which rows it uses, through optional PARTITION BY (groups), ORDER BY (order inside the group) and a frame (which nearby rows).

What is ROW_NUMBER?

ROW_NUMBER() gives each row in its window a unique sequential number starting at 1, with no ties and no gaps.

What is the difference between RANK and DENSE_RANK?

Both give tied rows the same rank. RANK then skips numbers (1, 1, 3) while DENSE_RANK does not (1, 1, 2).

What is PARTITION BY?

PARTITION BY splits the rows into groups so the window function restarts in each group. It is similar to GROUP BY but keeps all the rows.

What are LAG and LEAD?

LAG returns a value from an earlier row and LEAD from a later row in the same ordered window. They are used for comparisons such as this month versus last month.

Why are window functions important?

They are how SQL produces rankings, running totals, moving averages, shares of a total and period-over-period comparisons, which most reports need.

Are window functions asked in SQL interviews?

Yes. Ranking, top N per group, running totals and LAG/LEAD comparisons are among the most common interview topics for analyst, BI and data engineering roles.