SQL Window Functions โ Complete Guide
SQL Window Functions โ Complete Guide ๐ช
Window Functions โ modern SQL ka superpower. Ranking, running totals, moving averages, previous/next row access โ sab kuch bina GROUP BY ke, rows retain karke. Basic OVER() se lekar advanced frames tak โ professional queries ke saath complete deep dive. Data Insights par.
๐ Is Chapter Mein Kya Sikhenge:
- ๐ข Foundation: Introduction, OVER() clause, Sample Dataset
- ๐ก Ranking Functions: ROW_NUMBER, RANK, DENSE_RANK, NTILE
- ๐ด Advanced (Part B): LAG/LEAD, FIRST_VALUE/LAST_VALUE, Aggregate Windows, Frames, Real Scenarios
Topic 1: Window Functions Kya Hain โ Introduction
๐ Definition: Window Functions perform calculations across a set of table rows that are related to the current row โ called a "window." Unlike GROUP BY which collapses rows into groups, Window Functions retain ALL individual rows while adding calculated values as new columns. They use the OVER() clause to define the window (subset of rows to operate on).
๐ฏ Explanation: GROUP BY mein 100 rows โ 5 groups (rows collapse ho jaati hain โ details lose). Window Functions mein 100 rows โ 100 rows PLUS har row ke saath ek extra calculated column. Rows retain hoti hain! Example: "Har employee ke saath uske department ki average salary bhi dikhao" โ GROUP BY se nahi ho sakta (rows collapse hongi), Window Function se easily ho jaayega. Yeh modern SQL ka most powerful feature hai โ data analysis mein bahut use hota hai.
๐ GROUP BY vs Window Function โ Key Difference:
| Feature | GROUP BY | Window Function |
|---|---|---|
| Rows Output | Collapses โ one row per group | Retains โ all individual rows |
| Detail Level | Summary only | Row detail + summary together |
| Use Case | Aggregated reports | Row-level analytics with context |
| Example | "Total sales per region" (5 rows) | "Each sale + its region's total" (100 rows) |
๐ Types of Window Functions:
3 Categories of Window Functions:
RANKING FUNCTIONS
โข ROW_NUMBER() โ unique sequential numbers
โข RANK() โ gaps in ties (1, 2, 2, 4)
โข DENSE_RANK() โ no gaps (1, 2, 2, 3)
โข NTILE(n) โ divide into n buckets
VALUE FUNCTIONS
โข LAG(col) โ previous row's value
โข LEAD(col) โ next row's value
โข FIRST_VALUE(col) โ first value in window
โข LAST_VALUE(col) โ last value in window
โข NTH_VALUE(col,n) โ Nth value in window
AGGREGATE WINDOW FUNCTIONS
โข SUM() OVER() โ running total
โข AVG() OVER() โ moving average
โข COUNT() OVER() โ cumulative count
โข MIN/MAX OVER() โ running min/max
Topic 2: OVER() Clause โ The Heart of Window Functions
๐ Definition: The OVER() clause defines the "window" โ the set of rows the function operates on. Without OVER(), any function becomes a regular function or aggregate. OVER() can be empty (entire result set as window) or contain PARTITION BY (divide into groups) and ORDER BY (sort within groups). This is the CORE of window functions.
๐ฏ Explanation: OVER() = "window kya hai?" batata hai. 3 modes hain: (1) OVER() empty โ poora result set ek window. (2) OVER(PARTITION BY col) โ groups mein baanto (GROUP BY jaisa but rows retain). (3) OVER(ORDER BY col) โ sort karo, running calculations enable karo. Aur dono combine kar sakte ho. Yeh OVER() clause samajh gaye toh window functions ka 70% kaam ho gaya!
๐ OVER() Clause Syntax:
Basic Syntax:
window_function() OVER (
[PARTITION BY column1, column2, ...]
[ORDER BY column3 [ASC|DESC], ...]
[frame_clause]
)
Parts Explained:
โข PARTITION BY โ divide rows into groups (like GROUP BY but retains rows)
โข
ORDER BY โ sort rows within each partition
โข frame_clause โ subset of window (advanced โ covered later)
3 Common Patterns:
Pattern 1 โ Entire table as window:
SUM(amount) OVER ()
Pattern 2 โ
Group by region:
SUM(amount) OVER (PARTITION BY region)
Pattern 3 โ Running total by date:
SUM(amount) OVER (
PARTITION BY region
ORDER BY sale_date
)
๐ป Basic Examples โ Understanding OVER():
-- Example 1: OVER() empty โ entire result set as window
SELECT product_name, amount, SUM(amount) OVER () AS total_sales,
AVG(amount) OVER () AS avg_sales
FROM sales;| product_name | amount | total_sales | avg_sales |
|---|---|---|---|
| Laptop | 55000 | 245000 | 49000 |
| Mouse | 2000 | 245000 | 49000 |
| Keyboard | 3500 | 245000 | 49000 |
| Monitor | 18000 | 245000 | 49000 |
| Chair | 8000 | 245000 | 49000 |
Same total in every row! All rows retained + summary added to each row!
-- Example 2: OVER(PARTITION BY) โ grouped windows
SELECT product_name, category, amount, SUM(amount) OVER ( PARTITION BY category ) AS category_total
FROM sales;| product_name | category | amount | category_total |
|---|---|---|---|
| Chair | Furniture | 8000 | 8000 |
| Laptop | Electronics | 55000 | 73000 |
| Monitor | Electronics | 18000 | 73000 |
| Keyboard | Accessories | 3500 | 5500 |
| Mouse | Accessories | 2000 | 5500 Grouped by category โ but rows retained! |
All Electronics = 73000 All Accessories = 5500
๐ป GROUP BY vs Window Function โ Same Query, Different Output:
-- WITH GROUP BY (rows collapse!)
SELECT category, SUM(amount) AS category_total
FROM sales
GROUP BY category;
-- WITH WINDOW FUNCTION (rows retained!)
SELECT
product_name,
category,
amount,
SUM(amount) OVER (
PARTITION BY category
) AS category_total
FROM sales;
GROUP BY Output (3 rows): Window Function Output (5 rows):
| category | total |
|---|---|
| Accessories | 5500 |
| Electronics | 73000 |
| Furniture | 8000 |
| product_name | category | amount | total |
|---|---|---|---|
| Chair | Furniture | 8000 | 8000 |
| Laptop | Electronics | 55000 | 73000 |
| Monitor | Electronics | 18000 | 73000 |
| Keyboard | Accessories | 3500 | 5500 |
| Mouse | Accessories | 2000 | 5500 |
โข OVER() clause bhoolna โ
SUM(amount) aggregate hai, SUM(amount) OVER() window function hai. OVER() zaroori hai window function banane ke liye.โข PARTITION BY aur GROUP BY confuse karna โ GROUP BY rows collapse karta hai, PARTITION BY rows retain karta hai window ke andar.
Topic 3: Sample Dataset โ Sales Data
๐ Definition: A consistent sample dataset that we'll use throughout ALL window function examples. This is a realistic Sales table with products, categories, regions, salesperson, amount, and date โ perfect for demonstrating rankings, running totals, MoM growth, and other window function use cases.
๐ฏ Explanation: Isi dataset ko hum saare topics mein use karenge โ ROW_NUMBER, RANK, LAG, LEAD, Running Total sab. Data mein multiple categories, regions, salespersons, aur dates hain โ real-world jaisa. Yeh dataset ek baar create kar lo, phir saari queries directly copy karke chala sakte ho.
๐ป Create the Sales Table:
-- Create Sales table
CREATE TABLE sales ( sale_id INT PRIMARY KEY, product_name VARCHAR(100), category VARCHAR(50), region VARCHAR(50), salesperson VARCHAR(100), amount INT, sale_date DATE );
-- Insert sample data
INSERT INTO sales
(sale_id, product_name, category, region, salesperson, amount, sale_date)
VALUES
(1, 'Laptop', 'Electronics', 'North', 'Ravi', 55000, '2026-01-05'),
(2, 'Monitor', 'Electronics', 'South', 'Sneha', 18000, '2026-01-10'),
(3, 'Keyboard', 'Accessories', 'North', 'Ravi', 3500, '2026-01-15'),
(4, 'Mouse', 'Accessories', 'South', 'Sneha', 2000, '2026-01-20'),
(5, 'Chair', 'Furniture', 'East', 'Arjun', 8000, '2026-02-05'),
(6, 'Desk', 'Furniture', 'East', 'Arjun', 12000, '2026-02-10'),
(7, 'Laptop', 'Electronics', 'West', 'Priya', 60000, '2026-02-15'),
(8, 'Tablet', 'Electronics', 'North', 'Ravi', 25000, '2026-02-20'),
(9, 'Headphones','Accessories', 'West', 'Priya', 4500, '2026-03-05'),
(10, 'Sofa', 'Furniture', 'South', 'Sneha', 35000, '2026-03-10'),
(11, 'Monitor', 'Electronics', 'East', 'Arjun', 22000, '2026-03-15'),
(12, 'Keyboard', 'Accessories', 'West', 'Priya', 3500, '2026-03-20');
๐ View the Sales Data:
SELECT *
FROM sales
ORDER BY sale_id; | sale_id | product_name | category | region | salesperson | amount | sale_date |
|---|---|---|---|---|---|---|
| 1 | Laptop | Electronics | North | Ravi | 55000 | 2026-01-05 |
| 2 | Monitor | Electronics | South | Sneha | 18000 | 2026-01-10 |
| 3 | Keyboard | Accessories | North | Ravi | 3500 | 2026-01-15 |
| 4 | Mouse | Accessories | South | Sneha | 2000 | 2026-01-20 |
| 5 | Chair | Furniture | East | Arjun | 8000 | 2026-02-05 |
| 6 | Desk | Furniture | East | Arjun | 12000 | 2026-02-10 |
| 7 | Laptop | Electronics | West | Priya | 60000 | 2026-02-15 |
| 8 | Tablet | Electronics | North | Ravi | 25000 | 2026-02-20 |
| 9 | Headphones | Accessories | West | Priya | 4500 | 2026-03-05 |
| 10 | Sofa | Furniture | South | Sneha | 35000 | 2026-03-10 |
| 11 | Monitor | Electronics | East | Arjun | 22000 | 2026-03-15 |
| 12 | Keyboard | Accessories | West | Priya | 3500 | 2026-03-20 |
| 12 | rows | | 3 | categories | | 4 | regions | |
4 salespersons | Jan-Mar 2026
โข Categories: Electronics, Accessories, Furniture
โข Regions: North, South, East, West
โข Salespersons: Ravi, Sneha, Arjun, Priya
โข Date Range: January to March 2026
โข Amount Range: โน2,000 to โน60,000
Perfect for demonstrating rankings, groupings, time-based calculations!
Topic 4: ROW_NUMBER() โ Unique Sequential Numbers
๐ Definition: ROW_NUMBER() assigns a UNIQUE sequential integer to each row within a partition, starting from 1. Even if two rows have IDENTICAL values, they get DIFFERENT row numbers (based on ORDER BY sequence). It's the most commonly used ranking function โ perfect for "top N per group", "remove duplicates", and "select every Nth row" scenarios.
๐ฏ Explanation: ROW_NUMBER() = serial number dena โ 1, 2, 3, 4, 5... Har row ko unique number milta hai, DUPLICATES nahi hote. PARTITION BY se groups banao โ har group mein 1 se restart hoga. ORDER BY se decide karo kaunsa row pehla ho (usually amount DESC โ highest first). Real use cases: "Har region ka top seller nikalo", "Duplicate rows delete karo (row_num > 1 rakhne wale)", "Pagination โ 10 rows per page".
๐ Syntax:
ROW_NUMBER() OVER ( [PARTITION BY column_name] ORDER BY column_name [ASC | DESC] )
Rules:
โข
ORDER BY is REQUIRED โ without it, row
order is unpredictable
โข PARTITION BY is OPTIONAL โ if omitted, entire table is one partition
โข Returns unique 1, 2, 3, 4... within each partition
โข Ties in
ORDER BY are broken arbitrarily (unique numbers regardless)
๐ป Example 1 โ Basic Row Numbering:
-- Assign row numbers to all sales ordered by amount (highest first)
SELECT ROW_NUMBER() OVER ( ORDER BY amount DESC ) AS row_num,
product_name, category, amount
FROM sales; | row_num | product_name | category | amount |
|---|---|---|---|
| 1 | Laptop | Electronics | 60000 |
| โ Highest | 2 | Laptop | |
| Electronics | 55000 | 3 | Sofa |
| Furniture | 35000 | 4 | Tablet |
| Electronics | 25000 | 5 | Monitor |
| Electronics | 22000 | 6 | Monitor |
| Electronics | 18000 | 7 | Desk |
| Furniture | 12000 | 8 | Chair |
| Furniture | 8000 | 9 | Headphones |
| Accessories | 4500 | 10 | Keyboard |
| Accessories | 3500 | โ Tie with row | 11 |
| 11 | Keyboard | ||
| Accessories | 3500 | โ Same amount but different row_num! | 12 |
| Mouse | Accessories | 2000 โ Lowest |
๐ป Example 2 โ Row Numbers Per Category (PARTITION BY):
-- Rank products within each category (highest amount first)
SELECT category, product_name, amount, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY amount DESC ) AS rank_in_category
FROM sales
ORDER BY category, rank_in_category; | category | product_name | amount | rank_in_category |
|---|---|---|---|
| Accessories | Headphones | 4500 | 1 โ Top in Accessories |
| Accessories | Keyboard | 3500 | 2 |
| Accessories | Keyboard | 3500 | 3 โ Same amount, different rank Accessories |
| Mouse | 2000 | 4 | Electronics |
| Laptop | 60000 | 1 โ Top in Electronics | Electronics |
| Laptop | 55000 | 2 | Electronics |
| Tablet | 25000 | 3 | Electronics |
| Monitor | 22000 | 4 | Electronics |
| Monitor | 18000 | 5 | Furniture |
| Sofa | 35000 | 1 โ Top in Furniture | Furniture |
| Desk | 12000 | 2 | Furniture |
Each category restarts from 1! Chair 8000 3
๐ป Example 3 โ Top N Per Group (Real Interview Question):
-- Find top 2 products in each category by amount WITH ranked_sales AS (
SELECT category, product_name, amount, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY amount DESC ) AS rn
FROM sales ) SELECT category, product_name, amount, rn AS position
FROM ranked_sales
WHERE rn <= 2
ORDER BY category, rn; | category | product_name | amount | position |
|---|---|---|---|
| Accessories | Headphones | 4500 | 1 |
| โ Top | 2 | Accessories Accessories | Keyboard |
| 3500 | 2 | Electronics | Laptop |
| 60000 | 1 | โ Top | 2 |
| Electronics Electronics | Laptop | 55000 | 2 |
| Furniture | Sofa | 35000 | 1 |
| โ Top | 2 | Furniture Furniture | Desk |
Top 2 per category โ using ROW_NUMBER + WHERE! 12000 2
โข ORDER BY skip karna โ
ROW_NUMBER() OVER() without ORDER BY unpredictable results deta hai. Hamesha ORDER BY specify karo.โข Top N filtering WHERE mein directly likhna โ
WHERE ROW_NUMBER() OVER (...) <= 2 ERROR! Window functions WHERE clause mein directly use nahi hote โ pehle CTE ya subquery mein calculate karo, phir outer query mein WHERE lagao. Topic 5: RANK() vs DENSE_RANK() โ Handle Ties
๐ Definition: Both RANK() and DENSE_RANK() assign rank based on ORDER BY values. Key difference: when TIES occur (equal values), RANK() leaves GAPS in the sequence (1, 2, 2, 4 โ skips 3), while DENSE_RANK() gives CONSECUTIVE ranks (1, 2, 2, 3 โ no gaps). ROW_NUMBER() gives unique numbers regardless of ties.
๐ฏ Explanation: Teen ranking functions ka comparison โ ROW_NUMBER (hamesha unique), RANK (ties same rank, gap SKIP), DENSE_RANK (ties same rank, NO gap). Real example: 3 employees ka salary 100K hai โ teeno ka rank same hona chahiye. RANK() teeno ko rank 1 dega, next employee ko rank 4 (rank 2, 3 skip). DENSE_RANK() teeno ko rank 1 dega, next employee ko rank 2 (no skip). Interview mein Nth highest salary question โ DENSE_RANK sabse safe hai kyunki ties handle karta hai bina gaps ke.
๐ Comparison โ ROW_NUMBER vs RANK vs DENSE_RANK:
| Function | Tie Handling | Sequence Example |
|---|---|---|
| ROW_NUMBER() | Always unique โ no ties | 1, 2, 3, 4, 5 |
| RANK() | Same rank for ties, GAPS after | 1, 2, 2, 4, 5 (skips 3) |
| DENSE_RANK() | Same rank for ties, NO gaps | 1, 2, 2, 3, 4 (no skip) |
๐ป Example 1 โ Compare All 3 Functions Side by Side:
-- Compare ROW_NUMBER, RANK, DENSE_RANK on same data
SELECT product_name, amount, ROW_NUMBER() OVER (ORDER BY amount DESC) AS row_num,
RANK() OVER (ORDER BY amount DESC) AS rank_num, DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rank
FROM sales
ORDER BY amount DESC; Notice the difference at Mouse:
โข row_num: 12 (always unique)
โข rank_num: 12 (skipped 11 because tie above)
โข dense_rank: 11 (no gap)
๐ป Example 2 โ Nth Highest Salary (Classic Interview Question):
-- Find the 3rd highest sale amount (using DENSE_RANK) WITH ranked_sales AS (
SELECT product_name, amount, DENSE_RANK() OVER ( ORDER BY amount DESC ) AS rnk
FROM sales ) SELECT product_name, amount
FROM ranked_sales
WHERE rnk = 3; DENSE_RANK is safer than RANK for "Nth highest" queries
โ handles ties without skipping ranks!
๐ป Example 3 โ Ranking Per Category:
-- Rank salespersons by total sales within each region
SELECT region, salesperson, SUM(amount) AS total_sales, DENSE_RANK() OVER ( PARTITION BY region ORDER BY SUM(amount) DESC ) AS rank_in_region
FROM sales
GROUP BY region, salesperson
ORDER BY region, rank_in_region; Each region has one top performer!
โข RANK() ka use "Nth highest" queries mein โ agar ties hain toh gap ki wajah se rank N kabhi exist hi nahi karega. Example: 1, 2, 2, 4 mein "rank = 3" query kuch return nahi karegi! DENSE_RANK safer hai โ 1, 2, 2, 3 pattern.
โข Kaunsa use kab karein? Sports rankings (Olympic medals) โ RANK. Nth highest queries โ DENSE_RANK. Serial numbers/pagination โ ROW_NUMBER.
Topic 6: NTILE() โ Divide into N Equal Buckets
๐ Definition: NTILE(n) divides ordered rows into n approximately equal groups (buckets) and assigns a bucket number (1 to n) to each row. If rows can't be divided equally, earlier buckets get one extra row. Common uses: quartiles (NTILE(4)), deciles (NTILE(10)), percentiles (NTILE(100)) โ perfect for customer segmentation, performance categorization, and data distribution analysis.
๐ฏ Explanation: NTILE(4) matlab data ko 4 equal buckets mein baanto โ bucket 1 = top 25%, bucket 2 = 25-50%, etc. NTILE(10) = deciles, NTILE(100) = percentiles. Real business use: "Top 25% customers identify karo" (NTILE(4) where bucket = 1). "Salary quartiles" โ top earners kaun hain. "Product performance tiers" โ A/B/C category products. HR mein "performance ranking" ke liye bhi use hota hai โ top 20% performers.
๐ How NTILE Distributes Rows:
NTILE Distribution Examples:
12 rows into NTILE(4): 10 rows into NTILE(3):
Bucket 1: 3 rows Bucket 1: 4 rows (extra 1)
Bucket 2: 3 rows Bucket 2: 3 rows
Bucket 3: 3 rows Bucket 3: 3 rows
Bucket 4: 3 rows (10รท3 = 3.33, so first bucket gets extra)
11 rows into NTILE(4):
Bucket 1: 3 rows (extra 1)
Bucket 2: 3 rows (extra 1)
Bucket 3: 3 rows (extra 1) Wait, 3+3+3+2 = 11 โ
Bucket 4: 2 rows
Rule: Earlier buckets get extra rows if not divisible evenly
๐ป Example 1 โ Divide Sales into 4 Buckets (Quartiles):
-- Divide sales into 4 quartiles based on amount
SELECT product_name, amount, NTILE(4) OVER ( ORDER BY amount DESC ) AS quartile
FROM sales
ORDER BY amount DESC; 12 rows รท 4 buckets = 3 rows per bucket โ
๐ป Example 2 โ Top 25% High-Value Sales:
-- Find TOP 25% sales (highest quartile only) WITH quartiles AS (
SELECT product_name, category, region, amount, NTILE(4) OVER ( ORDER BY amount DESC ) AS quartile
FROM sales ) SELECT product_name, category, region, amount
FROM quartiles
WHERE quartile = 1
ORDER BY amount DESC; Top 25% high-value sales โ target for premium marketing!
๐ป Example 3 โ Performance Tiers (A/B/C Category):
-- Categorize sales into A (top), B (middle), C (bottom) tiers
SELECT product_name, category, amount, CASE NTILE(3) OVER (ORDER BY amount DESC)
WHEN 1
THEN 'A - High Value'
WHEN 2
THEN 'B - Medium Value'
WHEN 3
THEN 'C - Low Value'
END AS tier
FROM sales
ORDER BY amount DESC; Product tier categorization โ ready for marketing strategy!
โข NTILE ke saath ORDER BY zaroori hai โ bina order ke random buckets banenge.
โข NTILE ties handle nahi karta smartly โ same amount wale rows different buckets mein ja sakte hain (yeh common gotcha hai). Agar ties matter karti hain toh DENSE_RANK better hai NTILE se.
โข Bucket count sensibly choose karo โ NTILE(4) 12 rows par sahi, but NTILE(10) sirf 12 rows par uneven distribution dega.
Topic 7: LAG() and LEAD() โ Previous/Next Row Access
๐ Definition: LAG(column, offset, default) accesses a value from a PREVIOUS row without a self-join. LEAD(column, offset, default) accesses a value from the NEXT row. Both take 3 parameters: the column to fetch, how many rows back/forward (default 1), and a default value if no such row exists (default NULL). These are essential for period-over-period comparisons โ MoM growth, YoY comparison, previous vs current.
๐ฏ Explanation: LAG = "pichli row ki value dikhao" (jaise pichle month ki sales). LEAD = "agli row ki value dikhao" (next month ki sales). MoM growth calculate karne ke liye perfect โ (current - LAG(current)) / LAG(current) ร 100. Bina LAG/LEAD ke self-join karna padta โ complex aur slow. LAG/LEAD simple aur fast hai. Data analysts ke liye must-know function โ dashboards mein daily use hota hai.
๐ Syntax:
LAG(column, offset, default_value) OVER ( [PARTITION BY column_name] ORDER BY column_name )
LEAD(column, offset, default_value) OVER (
[PARTITION BY column_name]
ORDER BY column_name
)
Parameters:
โข column โ which column to fetch
โข
offset โ how many rows back/forward (default 1)
โข default_value โ what to return if no such row exists (default NULL)
๐ป Example 1 โ Basic LAG and LEAD:
-- Show current, previous, and next sale for each row
SELECT sale_id, product_name, amount, LAG(amount, 1, 0) OVER ( ORDER BY sale_id ) AS previous_amount,
LEAD(amount, 1, 0) OVER ( ORDER BY sale_id ) AS next_amount
FROM sales
ORDER BY sale_id;| sale_id | product_name | amount | previous_amount | next_amount |
|---|---|---|---|---|
| 1 | Laptop | 55000 | 0 (default โ no prev) | 18000 |
| 2 | Monitor | 18000 | 55000 | 3500 |
| 3 | Keyboard | 3500 | 18000 | 2000 |
| 4 | Mouse | 2000 | 3500 | 8000 |
| 5 | Chair | 8000 | 2000 | 12000 |
| 12 | Keyboard | 3500 | 22000 | 0 (default โ no next) |
๐ป Example 2 โ Month-over-Month Growth (Real Use Case):
-- Monthly sales with MoM growth percentage WITH monthly_sales AS (
SELECT DATE_FORMAT(sale_date, '%Y-%m') AS month, SUM(amount) AS total_sales
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m') ) SELECT month, total_sales,
LAG(total_sales, 1) OVER ( ORDER BY month ) AS prev_month_sales,
ROUND( (total_sales - LAG(total_sales) OVER (ORDER BY month)) * 100.0 / LAG(total_sales) OVER (ORDER BY month), 2 ) AS mom_growth_pct
FROM monthly_sales
ORDER BY month;| month | total_sales | prev_month_sales | mom_growth_pct |
|---|---|---|---|
| 2026-01 | 78500 | NULL | NULL (no prev month) |
| 2026-02 | 105000 | 78500 | +33.76% ๐ |
| 2026-03 | 65000 | 105000 | -38.10% ๐ |
๐ป Example 3 โ LAG with PARTITION BY (Per Category):
-- Previous sale amount within same category
SELECT category, product_name, sale_date, amount, LAG(amount, 1) OVER ( PARTITION BY category ORDER BY sale_date ) AS prev_sale_in_category
FROM sales
ORDER BY category, sale_date;| category | product_name | sale_date | amount | prev_sale_in_category |
|---|---|---|---|---|
| Accessories | Keyboard | 2026-01-15 | 3500 | NULL (first in category) |
| Accessories | Mouse | 2026-01-20 | 2000 | 3500 |
| Electronics | Laptop | 2026-01-05 | 55000 | NULL (first in category) |
| Electronics | Monitor | 2026-01-10 | 18000 | 55000 |
| Furniture | Chair | 2026-02-05 | 8000 | NULL (first in category) |
| Furniture | Desk | 2026-02-10 | 12000 | 8000 |
โข Default value nahi dena โ first row ka LAG NULL aayega, calculations mein error. Hamesha default do:
LAG(amount, 1, 0).โข ORDER BY galat dena โ LAG "previous" ka matlab depend karta hai order pe. Time-based analysis ke liye ORDER BY date use karo, na ki ORDER BY amount.
โข DIVIDE by zero avoid karo MoM growth mein โ agar previous month sales 0 hain toh division error. NULLIF ya CASE WHEN use karo.
Topic 8: FIRST_VALUE() and LAST_VALUE()
๐ Definition: FIRST_VALUE(column) returns the FIRST value in an ordered window/partition. LAST_VALUE(column) returns the LAST value in an ordered window/partition. Both are useful for comparing individual rows against the extreme values (first/last) of their group. IMPORTANT: LAST_VALUE requires an explicit frame clause โ without it, you get the wrong result (returns current row instead of actual last).
๐ฏ Explanation: FIRST_VALUE = "har row ke saath us group ki pehli value bhi dikhao." LAST_VALUE = "aakhri value dikhao." Real use: "Har category mein highest priced product ka naam dikhao har row ke saath." "Employee ka salary + department ka top earner ka salary โ compare karo." LAST_VALUE ke saath ek gotcha hai โ default frame current row tak hai, isliye actual last chahiye toh full frame specify karna padta hai: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
๐ป Example 1 โ Highest Amount Product per Category:
-- Show each product with the TOP selling product name in its category
SELECT category, product_name, amount, FIRST_VALUE(product_name) OVER ( PARTITION BY category ORDER BY amount DESC ) AS top_product,
FIRST_VALUE(amount) OVER ( PARTITION BY category ORDER BY amount DESC ) AS top_amount
FROM sales
ORDER BY category, amount DESC;| category | product_name | amount | top_product | top_amount |
|---|---|---|---|---|
| Accessories | Headphones | 4500 | Headphones | 4500 |
| Accessories | Keyboard | 3500 | Headphones | 4500 |
| Accessories | Mouse | 2000 | Headphones | 4500 |
| Electronics | Laptop | 60000 | Laptop | 60000 |
| Electronics | Laptop | 55000 | Laptop | 60000 |
| Furniture | Sofa | 35000 | Sofa | 35000 |
| Furniture | Desk | 12000 | Sofa | 35000 |
๐ป Example 2 โ LAST_VALUE Gotcha (Wrong vs Right):
-- WRONG WAY (returns current row, not actual last!)
SELECT category, product_name, amount, LAST_VALUE(product_name) OVER ( PARTITION BY category ORDER BY amount DESC ) AS wrong_last
FROM sales;
-- CORRECT WAY (with explicit frame)
SELECT
category,
product_name,
amount,
LAST_VALUE(product_name) OVER (
PARTITION BY category
ORDER BY amount DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS correct_lowest
FROM sales
ORDER BY category, amount DESC;
| category | product_name | amount | correct_lowest |
|---|---|---|---|
| Accessories | Headphones | 4500 | Mouse |
| Accessories | Keyboard | 3500 | Mouse |
| Accessories | Mouse | 2000 | Mouse |
| Electronics | Laptop | 60000 | Monitor |
| Electronics | Monitor | 18000 | Monitor |
๐ป Example 3 โ Compare with Highest/Lowest in Category:
-- Show each product's amount + highest + lowest in its category
SELECT category, product_name, amount, FIRST_VALUE(amount) OVER ( PARTITION BY category ORDER BY amount DESC ) AS highest_in_cat,
LAST_VALUE(amount) OVER ( PARTITION BY category ORDER BY amount DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS lowest_in_cat,
FIRST_VALUE(amount) OVER ( PARTITION BY category ORDER BY amount DESC ) - amount AS gap_from_top
FROM sales
ORDER BY category, amount DESC;| category | product_name | amount | highest_in_cat | lowest_in_cat | gap_from_top |
|---|---|---|---|---|---|
| Electronics | Laptop | 60000 | 60000 | 18000 | 0 |
| Electronics | Laptop | 55000 | 60000 | 18000 | 5000 |
| Electronics | Monitor | 18000 | 60000 | 18000 | 42000 |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, wrong results milte hain! Yeh interview mein bahut common trick question hai. Yaad rakho โ LAST_VALUE ke saath ALWAYS explicit frame do. Topic 9: NTH_VALUE() โ Specific Position Value
๐ Definition: NTH_VALUE(column, N) returns the value of the specified column from the Nth row within a window/partition. Unlike FIRST_VALUE (1st row) and LAST_VALUE (last row), NTH_VALUE gives you flexibility to fetch ANY specific position. It's less commonly used but powerful for scenarios like "show me the 2nd or 3rd highest value in each group."
๐ฏ Explanation: NTH_VALUE = specific position ka value nikaalo. FIRST_VALUE = 1st, LAST_VALUE = last, NTH_VALUE(col, 2) = 2nd, NTH_VALUE(col, 3) = 3rd, etc. Real use: "Har category ka 2nd highest product dikhao" โ perfect for medal-style rankings. Same LAST_VALUE gotcha โ explicit frame ki zaroorat padti hai proper output ke liye.
๐ป Example โ Show 2nd Highest in Each Category:
-- Show 1st, 2nd, and 3rd highest products in each category
SELECT DISTINCT category, FIRST_VALUE(product_name) OVER ( PARTITION BY category ORDER BY amount DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS top_1st,
NTH_VALUE(product_name, 2) OVER ( PARTITION BY category ORDER BY amount DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS top_2nd,
NTH_VALUE(product_name, 3) OVER ( PARTITION BY category ORDER BY amount DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS top_3rd
FROM sales;| category | top_1st | top_2nd | top_3rd |
|---|---|---|---|
| Accessories | Headphones | Keyboard | Keyboard |
| Electronics | Laptop | Laptop | Tablet |
| Furniture | Sofa | Desk | Chair |
โข NTH_VALUE ke saath bhi LAST_VALUE jaisa frame gotcha hai โ bina explicit frame ke wrong results.
โข Agar partition mein kam rows hain (jaise sirf 2 rows aur tumne NTH_VALUE(col, 3) maanga) toh NULL return hoga โ kuch categories mein 3rd position exist nahi karti.
Topic 10: Aggregate Window Functions โ Running Totals & Moving Averages
๐ Definition: Regular aggregate functions (SUM, AVG, COUNT, MIN, MAX) can be used AS WINDOW FUNCTIONS by adding the OVER() clause. This transforms them from collapsing rows (like GROUP BY) into keeping all rows while adding cumulative calculations. This enables Running Totals, Moving Averages, Cumulative Counts, and other time-series analytics โ extremely powerful for dashboards and trend analysis.
๐ฏ Explanation: Jab SUM/AVG/COUNT ke saath OVER() lagate hain โ yeh cumulative kaam karte hain instead of collapsing. Running Total = "abhi tak ka total" โ har row par pichli sab rows ka sum. Moving Average = "last N rows ka average" โ trends smooth karta hai. Business dashboards mein YTD sales, cumulative customer count, 7-day moving average โ sab yehi se banti hain. Data analysts ke liye MUST-KNOW.
๐ป Example 1 โ Running Total (Cumulative Sum):
-- Running total of sales by date
SELECT sale_id, sale_date, product_name, amount, SUM(amount) OVER ( ORDER BY sale_date, sale_id ) AS running_total
FROM sales
ORDER BY sale_date, sale_id;| sale_id | sale_date | product_name | amount | running_total |
|---|---|---|---|---|
| 1 | 2026-01-05 | Laptop | 55000 | 55000 |
| 2 | 2026-01-10 | Monitor | 18000 | 73000 |
| 3 | 2026-01-15 | Keyboard | 3500 | 76500 |
| 4 | 2026-01-20 | Mouse | 2000 | 78500 |
| 5 | 2026-02-05 | Chair | 8000 | 86500 |
| 12 | 2026-03-20 | Keyboard | 3500 | 248500 |
๐ป Example 2 โ Running Total Per Category:
-- Running total within each category
SELECT category, sale_date, product_name, amount, SUM(amount) OVER ( PARTITION BY category ORDER BY sale_date ) AS cat_running_total,
COUNT(*) OVER ( PARTITION BY category ORDER BY sale_date ) AS cat_running_count
FROM sales
ORDER BY category, sale_date;| category | sale_date | product_name | amount | cat_running_total | cat_running_count |
|---|---|---|---|---|---|
| Accessories | 2026-01-15 | Keyboard | 3500 | 3500 | 1 |
| Accessories | 2026-01-20 | Mouse | 2000 | 5500 | 2 |
| Accessories | 2026-03-05 | Headphones | 4500 | 10000 | 3 |
| Accessories | 2026-03-20 | Keyboard | 3500 | 13500 | 4 |
| Electronics | 2026-01-05 | Laptop | 55000 | 55000 | 1 |
| Electronics | 2026-01-10 | Monitor | 18000 | 73000 | 2 |
๐ป Example 3 โ Category Total Comparison + % of Total:
-- Show each sale amount + category total + % of category total
SELECT category, product_name, amount, SUM(amount) OVER ( PARTITION BY category ) AS category_total,
ROUND( amount * 100.0 / SUM(amount) OVER (PARTITION BY category), 2 ) AS pct_of_category,
ROUND( AVG(amount) OVER (PARTITION BY category), 0 ) AS category_avg
FROM sales
ORDER BY category, amount DESC;| category | product_name | amount | category_total | pct_of_category | category_avg |
|---|---|---|---|---|---|
| Accessories | Headphones | 4500 | 13500 | 33.33% | 3375 |
| Accessories | Keyboard | 3500 | 13500 | 25.93% | 3375 |
| Electronics | Laptop | 60000 | 180000 | 33.33% | 36000 |
| Electronics | Laptop | 55000 | 180000 | 30.56% | 36000 |
โข Running total ke liye ORDER BY zaroori hai โ bina ORDER BY ke SUM() OVER() sirf total sum dega har row mein (like OVER() empty).
โข PARTITION BY use karna bhoolna jab per-group running total chahiye โ poore data ka running total ban jaayega.
Topic 11: Window Frames โ ROWS/RANGE BETWEEN
๐ Definition: Window Frames define EXACTLY which rows are included in the window calculation for each row. By default, when you use ORDER BY without a frame, the frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. You can customize this with ROWS BETWEEN N PRECEDING AND M FOLLOWING to create Moving Averages (3-day, 7-day) or centered calculations. This is the most advanced window function concept.
๐ฏ Explanation: Frame = "window ke andar kaunse rows include karein calculation ke liye." Default: beginning se current row tak. Custom frames se moving average ban sakta hai โ "current row + last 2 rows ka average" (3-day moving avg). Bahut powerful for trend analysis โ daily fluctuations smooth karke long-term trends dikhata hai. Stock market, sales trends, KPI monitoring โ sab mein moving averages use hote hain.
๐ Frame Clause Syntax:
Frame Options:
ROWS BETWEEN AND
Common Frame Patterns:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
โ
From beginning to current row (default with ORDER BY)
โ Used for: Running Total
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
โ Current row + previous 2 rows (3 total)
โ Used for: 3-row Moving Average
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
โ Previous + current + next (centered 3 rows)
โ Used for: Centered smoothing
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
โ
From current to
end
โ Used for: "How much left after this row?"
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
โ Entire partition
โ Used for: Overall totals (fixed for LAST_VALUE)
๐ป Example 1 โ 3-Row Moving Average:
-- 3-row moving average of sales amount
SELECT sale_id, sale_date, amount, ROUND( AVG(amount) OVER ( ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 2 ) AS moving_avg_3row
FROM sales
ORDER BY sale_date;| sale_id | sale_date | amount | moving_avg_3row |
|---|---|---|---|
| 1 | 2026-01-05 | 55000 | 55000.00 (only 1 row) |
| 2 | 2026-01-10 | 18000 | 36500.00 (avg of 2 rows) |
| 3 | 2026-01-15 | 3500 | 25500.00 (avg of 3 rows) |
| 4 | 2026-01-20 | 2000 | 7833.33 (rolling 3) |
| 5 | 2026-02-05 | 8000 | 4500.00 (rolling 3) |
| 6 | 2026-02-10 | 12000 | 7333.33 (rolling 3) |
๐ป Example 2 โ Comparing Different Frame Types:
-- Compare different frame types side by side
SELECT sale_id, sale_date, amount, SUM(amount) OVER ( ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total,
SUM(amount) OVER ( ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS sum_prev_curr_next,
SUM(amount) OVER ( ORDER BY sale_date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ) AS remaining_from_here
FROM sales
ORDER BY sale_date;| sale_id | amount | running_total | sum_prev_curr_next | remaining_from_here |
|---|---|---|---|---|
| 1 | 55000 | 55000 | 73000 | 248500 |
| 2 | 18000 | 73000 | 76500 | 193500 |
| 3 | 3500 | 76500 | 23500 | 175500 |
| 4 | 2000 | 78500 | 13500 | 172000 |
โข ROWS vs RANGE confuse karna โ ROWS physical row count par based hai, RANGE value-based hai (same values wale rows include hote hain). Beginner ke liye ROWS use karo โ more predictable.
โข Moving average ke pehle rows mein incomplete data โ first 2 rows mein sirf 1 ya 2 row ka average aayega (3 nahi). Yeh expected behavior hai โ visualization mein handle karo.
Topic 12: Real Scenario Problems โ Interview Classics
๐ Definition: Real-world scenarios that combine multiple window functions concepts. These are the exact problems asked in data analyst interviews and needed in daily dashboard work. Master these 5 scenarios and you'll be able to solve 90% of window function problems in interviews and real projects.
๐ป Scenario 1 โ Top N Per Group (Top 2 sales per region):
-- Top 2 highest sales in each region WITH ranked_sales AS (
SELECT region, product_name, salesperson, amount, ROW_NUMBER() OVER ( PARTITION BY region ORDER BY amount DESC ) AS rn
FROM sales ) SELECT region, product_name, salesperson, amount
FROM ranked_sales
WHERE rn <= 2
ORDER BY region, amount DESC;| region | product_name | salesperson | amount |
|---|---|---|---|
| East | Monitor | Arjun | 22000 |
| East | Desk | Arjun | 12000 |
| North | Laptop | Ravi | 55000 |
| North | Tablet | Ravi | 25000 |
| South | Sofa | Sneha | 35000 |
| South | Monitor | Sneha | 18000 |
| West | Laptop | Priya | 60000 |
| West | Headphones | Priya | 4500 |
๐ป Scenario 2 โ Salesperson Total + Rank + % of Company Total:
-- Complete salesperson performance analysis
SELECT salesperson, SUM(amount) AS total_sales, COUNT(*) AS total_orders,
RANK() OVER ( ORDER BY SUM(amount) DESC ) AS sales_rank,
ROUND( SUM(amount) * 100.0 / SUM(SUM(amount)) OVER (), 2 ) AS pct_of_company
FROM sales
GROUP BY salesperson
ORDER BY total_sales DESC;| salesperson | total_sales | total_orders | sales_rank | pct_of_company |
|---|---|---|---|---|
| Ravi | 83500 | 3 | 1 | 33.60% |
| Priya | 68000 | 3 | 2 | 27.36% |
| Sneha | 55000 | 3 | 3 | 22.13% |
| Arjun | 42000 | 3 | 4 | 16.90% |
๐ป Scenario 3 โ Compare Current Sale with Previous (LAG):
-- Compare each sale with previous sale (day-over-day)
SELECT sale_id, sale_date, product_name, amount, LAG(amount, 1, 0) OVER ( ORDER BY sale_date, sale_id ) AS previous_amount,
amount - LAG(amount, 1, 0) OVER ( ORDER BY sale_date, sale_id ) AS change_amount,
CASE
WHEN amount > LAG(amount) OVER (ORDER BY sale_date, sale_id)
THEN '๐ Increased'
WHEN amount < LAG(amount) OVER (ORDER BY sale_date, sale_id)
THEN '๐ Decreased'
ELSE 'First Sale'
END AS trend
FROM sales
ORDER BY sale_date, sale_id;| sale_id | product_name | amount | previous_amount | change_amount | trend |
|---|---|---|---|---|---|
| 1 | Laptop | 55000 | 0 | 55000 | First Sale |
| 2 | Monitor | 18000 | 55000 | -37000 | ๐ Decreased |
| 5 | Chair | 8000 | 2000 | 6000 | ๐ Increased |
| 7 | Laptop | 60000 | 12000 | 48000 | ๐ Increased |
๐ป Scenario 4 โ Nth Highest Sale Per Category:
-- Find 2nd highest sale in each category (DENSE_RANK handles ties) WITH ranked_sales AS (
SELECT category, product_name, amount, DENSE_RANK() OVER ( PARTITION BY category ORDER BY amount DESC ) AS rnk
FROM sales ) SELECT category, product_name, amount
FROM ranked_sales
WHERE rnk = 2
ORDER BY category;| category | product_name | amount |
|---|---|---|
| Accessories | Keyboard | 3500 |
| Accessories | Keyboard | 3500 |
| Electronics | Laptop | 55000 |
| Furniture | Desk | 12000 |
๐ป Scenario 5 โ Cumulative % Contribution (Pareto Analysis):
-- 80/20 rule โ cumulative % of sales by product
SELECT product_name, amount, SUM(amount) OVER ( ORDER BY amount DESC ) AS running_total,
ROUND( SUM(amount) OVER (ORDER BY amount DESC) * 100.0 / SUM(amount) OVER (), 2 ) AS cumulative_pct
FROM sales
ORDER BY amount DESC;| product_name | amount | running_total | cumulative_pct |
|---|---|---|---|
| Laptop | 60000 | 60000 | 24.14% |
| Laptop | 55000 | 115000 | 46.28% |
| Sofa | 35000 | 150000 | 60.36% |
| Tablet | 25000 | 175000 | 70.42% |
| Monitor | 22000 | 197000 | 79.28% |
| Monitor | 18000 | 215000 | 86.52% |
Window Functions โ Complete Quick Reference
| Function | Purpose | Use Case |
|---|---|---|
| ROW_NUMBER() | Unique sequential numbers | Top N per group, duplicates removal |
| RANK() | Ties same rank, gaps after | Sports rankings, competition |
| DENSE_RANK() | Ties same rank, no gaps | Nth highest queries |
| NTILE(n) | Divide into N buckets | Quartiles, deciles, tiers |
| LAG(col) | Previous row value | MoM growth, day-over-day comparison |
| LEAD(col) | Next row value | Forecast comparison, next event |
| FIRST_VALUE(col) | First value in window | Compare with group's best/first |
| LAST_VALUE(col) | Last value in window (needs frame!) | Compare with group's worst/last |
| NTH_VALUE(col, N) | Nth position value (needs frame!) | 2nd or 3rd highest values |
| SUM() OVER() | Running total or group total | YTD sales, cumulative revenue |
| AVG() OVER() | Moving average | 7-day/30-day moving avg |
| ROWS BETWEEN | Custom frame definition | 3-day moving avg, custom windows |
๐ SQL Window Functions โ Complete!
12 topics mein SQL Window Functions ka complete deep dive โ Introduction se lekar Real Scenarios tak. ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE, Aggregate Windows, Frames, aur 5 real-world scenarios. Yeh SQL ka most powerful feature hai โ data analysts ke liye MUST-KNOW. Interviews mein 100% puchha jaata hai. Data Insights SQL Topic Wise ka doosra topic complete! Aage aur SQL topics aayenge โ JOINs Masterclass, Subqueries & CTEs, Date Functions, aur bahut kuch. Data Insights par sab FREE, detailed, Hinglish mein.
Happy Querying & Keep Analyzing! ๐
๐ฌ Comments (0)
Loading comments...