All 40 lessons

Lesson 26 of 40 · Window Functions

SQL Window Functions

By Skillancy Editorial Team

Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.

A window function calculates across a set of related rows but, unlike GROUP BY, keeps every row in the result. You can show each order next to the customer's total, each product's share of its category, or a running total, all without losing detail. Window functions come up in almost every data analyst interview.

Syntax

FUNCTION(column) OVER (
  PARTITION BY group_column   -- optional: restart per group
  ORDER BY sort_column        -- optional: order within the window
)
  • OVER () turns an ordinary function into a window function.
  • PARTITION BY splits rows into groups (windows). Without it, the window is all rows.
  • ORDER BY inside OVER sets the order within each window (needed for ranking and running totals).

GROUP BY vs window function

GROUP BY returns one row per customer:

SELECT customer_id, COUNT(*) AS orders
FROM orders
WHERE customer_id IN (1, 3)
GROUP BY customer_id;
Try it in the SQL Compiler

Result:

customer_idorders
14
33

(2 rows)

A window function keeps every order and adds the customer's count to each:

SELECT order_id,
       customer_id,
       order_date,
       COUNT(*) OVER (PARTITION BY customer_id) AS customer_orders
FROM orders
WHERE customer_id IN (1, 3)
ORDER BY customer_id, order_date;
Try it in the SQL Compiler

Result:

order_idcustomer_idorder_datecustomer_orders
100112025-01-054
100412025-02-204
101112025-06-304
101912025-11-154
100332025-02-023
100932025-05-213
102132025-12-203

(7 rows)

Examples

1. Grand total on every row

OVER () with nothing inside uses all rows:

SELECT product_name,
       stock_qty,
       SUM(stock_qty) OVER () AS total_stock,
       ROUND(stock_qty * 100.0 / SUM(stock_qty) OVER (), 1) AS pct_of_stock
FROM products
ORDER BY stock_qty DESC
LIMIT 5;
Try it in the SQL Compiler

Result:

product_namestock_qtytotal_stockpct_of_stock
Herbal Face Wash500179227.9
Steel Water Bottle 1L300179216.7
boAt Airdopes 141250179214.0
Yoga Mat 6mm15017928.4
The Psychology of Money12017926.7

(5 rows)

2. Per-group totals with PARTITION BY

Each product's price compared with its category average:

SELECT p.product_name,
       c.category_name,
       p.price,
       ROUND(AVG(p.price) OVER (PARTITION BY p.category_id), 0) AS category_avg,
       ROUND(p.price - AVG(p.price) OVER (PARTITION BY p.category_id), 0) AS vs_avg
FROM products p
JOIN categories c ON c.category_id = p.category_id
WHERE p.category_id IN (1, 3)
ORDER BY c.category_name, p.price DESC;
Try it in the SQL Compiler

Result:

product_namecategory_namepricecategory_avgvs_avg
Samsung 43" Smart TVElectronics31990.001909612894
Redmi Note 13 ProElectronics23999.00190964903
boAt Airdopes 141Electronics1299.0019096-17797
Prestige Pressure Cooker 5LHome & Kitchen2199.001216983
Non-stick TawaHome & Kitchen899.001216-317
Steel Water Bottle 1LHome & Kitchen549.001216-667

(6 rows)

This replaces the correlated subquery from the previous module with something shorter and faster.

3. Share of category revenue

WITH product_revenue AS (
  SELECT p.product_name,
         p.category_id,
         SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100)) AS revenue
  FROM order_items oi
  JOIN products p ON p.product_id = oi.product_id
  GROUP BY p.product_name, p.category_id
)
SELECT product_name,
       category_id,
       ROUND(revenue, 0) AS revenue,
       ROUND(revenue * 100 / SUM(revenue) OVER (PARTITION BY category_id), 1) AS pct_of_category
FROM product_revenue
WHERE category_id IN (2, 3)
ORDER BY category_id, pct_of_category DESC;
Try it in the SQL Compiler

Result:

product_namecategory_idrevenuepct_of_category
Women's Silk Saree22422263.0
Men's Cotton Kurta2750119.5
Running Shoes2674717.5
Steel Water Bottle 1L3494138.5
Prestige Pressure Cooker 5L3428833.4
Non-stick Tawa3359628.0

(6 rows)

Notice the pattern: aggregate with GROUP BY in a CTE, then apply window functions on top.

4. ORDER BY inside OVER: a running count

With ORDER BY, the window grows row by row:

SELECT order_id,
       order_date,
       COUNT(*) OVER (ORDER BY order_date) AS orders_to_date
FROM orders
ORDER BY order_date
LIMIT 6;
Try it in the SQL Compiler

Result:

order_idorder_dateorders_to_date
10012025-01-051
10022025-01-182
10032025-02-023
10042025-02-204
10052025-03-115
10062025-03-296

(6 rows)

Running totals get their own lesson: running totals and moving averages.

5. Filtering on a window result

Window functions run after WHERE, so filter in an outer query:

WITH priced AS (
  SELECT product_name,
         category_id,
         price,
         AVG(price) OVER (PARTITION BY category_id) AS category_avg
  FROM products
)
SELECT product_name, category_id, price, ROUND(category_avg, 0) AS category_avg
FROM priced
WHERE price > category_avg
ORDER BY category_id;
Try it in the SQL Compiler

Result:

product_namecategory_idpricecategory_avg
Redmi Note 13 Pro123999.0019096
Samsung 43" Smart TV131990.0019096
Women's Silk Saree28499.004299
Prestige Pressure Cooker 5L32199.001216
Kumkumadi Face Oil42995.001597
Wings of Fire5350.00325
Cricket Bat (Kashmir Willow)63499.002149

(7 rows)

The main window functions

FamilyFunctionsLesson
AggregateSUM, AVG, COUNT, MIN, MAXthis one
RankingROW_NUMBER, RANK, DENSE_RANK, NTILEranking functions
OffsetLAG, LEAD, FIRST_VALUE, LAST_VALUELAG and LEAD

Common mistakes

Using a window function in WHERE:

SELECT product_name
FROM products
WHERE price > AVG(price) OVER (PARTITION BY category_id);
Try it in the SQL Compiler

Error: window functions are not allowed in WHERE

  • Expecting fewer rows. Window functions never remove rows; use GROUP BY if you want one row per group.
  • Forgetting that ORDER BY changes the frame. SUM(x) OVER (ORDER BY d) is a running total, not the group total.

Practice question

For each employee, show their salary, their department's average salary, and the difference.

Show solution
SELECT employee_name,
       department,
       salary,
       ROUND(AVG(salary) OVER (PARTITION BY department), 0) AS dept_avg,
       ROUND(salary - AVG(salary) OVER (PARTITION BY department), 0) AS diff
FROM employees
ORDER BY department, salary DESC
LIMIT 6;
Try it in the SQL Compiler

Result:

employee_namedepartmentsalarydept_avgdiff
Rajesh KhannaLeadership450000.004500000
Pooja SinhaMarketing180000.0011066769333
Tanvi ShahMarketing82000.00110667-28667
Sana QureshiMarketing70000.00110667-40667
Neha JoshiOperations220000.00112000108000
Suresh RaoOperations95000.00112000-17000

(6 rows)

Key takeaways

  • Window functions calculate across related rows without collapsing them.
  • OVER () = all rows; PARTITION BY = per group; ORDER BY inside = ordered/cumulative.
  • They're calculated after WHERE/GROUP BY, so filter on them in an outer query.
  • Aggregate in a CTE first, then apply window functions for shares and comparisons.
Want every variation on one page?Window functions in SQL: the complete guide

Frequently asked questions

What is the difference between GROUP BY and PARTITION BY?

GROUP BY collapses each group into one row. PARTITION BY in a window function calculates per group but keeps every original row, adding the result as a new column.

Can I use a window function in WHERE?

No. Window functions are calculated after WHERE, GROUP BY and HAVING. To filter on one, compute it in a CTE or subquery and filter in the outer query. Some databases, such as Snowflake and BigQuery, offer QUALIFY for this.

What happens when OVER() is empty?

The window is the whole result set, so SUM(x) OVER () gives the grand total on every row.

Newsletter

Keep learning SQL

One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Prefer a one-page summary? Download the free SQL cheat sheet (PDF)