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 BYsplits rows into groups (windows). Without it, the window is all rows.ORDER BYinsideOVERsets 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 CompilerResult:
| customer_id | orders |
|---|---|
| 1 | 4 |
| 3 | 3 |
(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 CompilerResult:
| order_id | customer_id | order_date | customer_orders |
|---|---|---|---|
| 1001 | 1 | 2025-01-05 | 4 |
| 1004 | 1 | 2025-02-20 | 4 |
| 1011 | 1 | 2025-06-30 | 4 |
| 1019 | 1 | 2025-11-15 | 4 |
| 1003 | 3 | 2025-02-02 | 3 |
| 1009 | 3 | 2025-05-21 | 3 |
| 1021 | 3 | 2025-12-20 | 3 |
(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 CompilerResult:
| product_name | stock_qty | total_stock | pct_of_stock |
|---|---|---|---|
| Herbal Face Wash | 500 | 1792 | 27.9 |
| Steel Water Bottle 1L | 300 | 1792 | 16.7 |
| boAt Airdopes 141 | 250 | 1792 | 14.0 |
| Yoga Mat 6mm | 150 | 1792 | 8.4 |
| The Psychology of Money | 120 | 1792 | 6.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 CompilerResult:
| product_name | category_name | price | category_avg | vs_avg |
|---|---|---|---|---|
| Samsung 43" Smart TV | Electronics | 31990.00 | 19096 | 12894 |
| Redmi Note 13 Pro | Electronics | 23999.00 | 19096 | 4903 |
| boAt Airdopes 141 | Electronics | 1299.00 | 19096 | -17797 |
| Prestige Pressure Cooker 5L | Home & Kitchen | 2199.00 | 1216 | 983 |
| Non-stick Tawa | Home & Kitchen | 899.00 | 1216 | -317 |
| Steel Water Bottle 1L | Home & Kitchen | 549.00 | 1216 | -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 CompilerResult:
| product_name | category_id | revenue | pct_of_category |
|---|---|---|---|
| Women's Silk Saree | 2 | 24222 | 63.0 |
| Men's Cotton Kurta | 2 | 7501 | 19.5 |
| Running Shoes | 2 | 6747 | 17.5 |
| Steel Water Bottle 1L | 3 | 4941 | 38.5 |
| Prestige Pressure Cooker 5L | 3 | 4288 | 33.4 |
| Non-stick Tawa | 3 | 3596 | 28.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 CompilerResult:
| order_id | order_date | orders_to_date |
|---|---|---|
| 1001 | 2025-01-05 | 1 |
| 1002 | 2025-01-18 | 2 |
| 1003 | 2025-02-02 | 3 |
| 1004 | 2025-02-20 | 4 |
| 1005 | 2025-03-11 | 5 |
| 1006 | 2025-03-29 | 6 |
(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 CompilerResult:
| product_name | category_id | price | category_avg |
|---|---|---|---|
| Redmi Note 13 Pro | 1 | 23999.00 | 19096 |
| Samsung 43" Smart TV | 1 | 31990.00 | 19096 |
| Women's Silk Saree | 2 | 8499.00 | 4299 |
| Prestige Pressure Cooker 5L | 3 | 2199.00 | 1216 |
| Kumkumadi Face Oil | 4 | 2995.00 | 1597 |
| Wings of Fire | 5 | 350.00 | 325 |
| Cricket Bat (Kashmir Willow) | 6 | 3499.00 | 2149 |
(7 rows)
The main window functions
| Family | Functions | Lesson |
|---|---|---|
| Aggregate | SUM, AVG, COUNT, MIN, MAX | this one |
| Ranking | ROW_NUMBER, RANK, DENSE_RANK, NTILE | ranking functions |
| Offset | LAG, LEAD, FIRST_VALUE, LAST_VALUE | LAG 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 CompilerError: window functions are not allowed in WHERE
- Expecting fewer rows. Window functions never remove rows; use
GROUP BYif 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 CompilerResult:
| employee_name | department | salary | dept_avg | diff |
|---|---|---|---|---|
| Rajesh Khanna | Leadership | 450000.00 | 450000 | 0 |
| Pooja Sinha | Marketing | 180000.00 | 110667 | 69333 |
| Tanvi Shah | Marketing | 82000.00 | 110667 | -28667 |
| Sana Qureshi | Marketing | 70000.00 | 110667 | -40667 |
| Neha Joshi | Operations | 220000.00 | 112000 | 108000 |
| Suresh Rao | Operations | 95000.00 | 112000 | -17000 |
(6 rows)
Key takeaways
- Window functions calculate across related rows without collapsing them.
OVER ()= all rows;PARTITION BY= per group;ORDER BYinside = 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.
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.
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)