Skip to content

Commit eadefd5

Browse files
Adez017sanjay-kv
andcommitted
Added window functions
Co-Authored-By: Sanjay Viswanathan <[email protected]>
1 parent 08124dd commit eadefd5

File tree

2 files changed

+1
-241
lines changed

2 files changed

+1
-241
lines changed

docs/sql/SQL-Advance/window-functions.md

Lines changed: 0 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -414,247 +414,6 @@ N FOLLOWING -- N rows after current
414414
</TabItem>
415415
</Tabs>
416416

417-
## Real-World Use Cases
418-
419-
:::success
420-
**Business Analytics & Reporting**
421-
422-
1. **Sales Performance Dashboard**
423-
```sql
424-
WITH daily_metrics AS (
425-
SELECT
426-
sale_date,
427-
salesperson_id,
428-
salesperson_name,
429-
region,
430-
SUM(sale_amount) AS daily_sales
431-
FROM sales
432-
WHERE sale_date >= '2024-01-01'
433-
GROUP BY sale_date, salesperson_id, salesperson_name, region
434-
)
435-
SELECT
436-
sale_date,
437-
salesperson_name,
438-
region,
439-
daily_sales,
440-
-- Regional context
441-
AVG(daily_sales) OVER (PARTITION BY region, sale_date) AS region_avg_today,
442-
RANK() OVER (PARTITION BY region, sale_date ORDER BY daily_sales DESC) AS daily_region_rank,
443-
-- Personal trends
444-
LAG(daily_sales) OVER (PARTITION BY salesperson_id ORDER BY sale_date) AS yesterday_sales,
445-
daily_sales - LAG(daily_sales) OVER (PARTITION BY salesperson_id ORDER BY sale_date) AS day_over_day_change,
446-
-- Running metrics
447-
SUM(daily_sales) OVER (
448-
PARTITION BY salesperson_id
449-
ORDER BY sale_date
450-
ROWS UNBOUNDED PRECEDING
451-
) AS ytd_sales,
452-
AVG(daily_sales) OVER (
453-
PARTITION BY salesperson_id
454-
ORDER BY sale_date
455-
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
456-
) AS rolling_7day_avg
457-
FROM daily_metrics
458-
ORDER BY sale_date DESC, region, daily_sales DESC;
459-
```
460-
461-
2. **Customer Segmentation**
462-
```sql
463-
WITH customer_stats AS (
464-
SELECT
465-
customer_id,
466-
customer_name,
467-
COUNT(*) AS total_orders,
468-
SUM(order_amount) AS lifetime_value,
469-
MAX(order_date) AS last_order_date,
470-
MIN(order_date) AS first_order_date,
471-
DATEDIFF(CURRENT_DATE, MAX(order_date)) AS days_since_last_order
472-
FROM orders
473-
GROUP BY customer_id, customer_name
474-
)
475-
SELECT
476-
customer_id,
477-
customer_name,
478-
lifetime_value,
479-
total_orders,
480-
days_since_last_order,
481-
-- Value segmentation
482-
NTILE(5) OVER (ORDER BY lifetime_value DESC) AS value_quintile,
483-
-- Recency scoring
484-
NTILE(5) OVER (ORDER BY days_since_last_order) AS recency_score,
485-
-- Frequency ranking
486-
PERCENT_RANK() OVER (ORDER BY total_orders) AS frequency_percentile,
487-
-- Overall ranking
488-
RANK() OVER (ORDER BY lifetime_value DESC) AS customer_rank,
489-
-- Compare to average
490-
lifetime_value - AVG(lifetime_value) OVER () AS value_vs_avg,
491-
CASE
492-
WHEN NTILE(4) OVER (ORDER BY lifetime_value DESC) = 1
493-
AND days_since_last_order < 90 THEN 'Champion'
494-
WHEN NTILE(4) OVER (ORDER BY lifetime_value DESC) = 1 THEN 'At Risk VIP'
495-
WHEN days_since_last_order < 30 THEN 'Active'
496-
WHEN days_since_last_order > 180 THEN 'Churned'
497-
ELSE 'Regular'
498-
END AS segment
499-
FROM customer_stats;
500-
```
501-
502-
3. **Inventory Management**
503-
```sql
504-
SELECT
505-
product_name,
506-
stock_date,
507-
quantity_sold,
508-
current_stock,
509-
reorder_point,
510-
-- Moving average demand
511-
AVG(quantity_sold) OVER (
512-
PARTITION BY product_name
513-
ORDER BY stock_date
514-
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
515-
) AS avg_daily_demand_30d,
516-
-- Days until reorder needed
517-
CASE
518-
WHEN AVG(quantity_sold) OVER (
519-
PARTITION BY product_name
520-
ORDER BY stock_date
521-
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
522-
) > 0 THEN
523-
FLOOR(current_stock / AVG(quantity_sold) OVER (
524-
PARTITION BY product_name
525-
ORDER BY stock_date
526-
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
527-
))
528-
ELSE NULL
529-
END AS days_of_stock_remaining,
530-
-- Stock status
531-
CASE
532-
WHEN current_stock < reorder_point THEN 'REORDER NOW'
533-
WHEN current_stock < reorder_point * 1.5 THEN 'Low Stock'
534-
ELSE 'Adequate'
535-
END AS stock_status
536-
FROM inventory_daily
537-
ORDER BY product_name, stock_date DESC;
538-
```
539-
:::
540-
541-
## Performance Tips
542-
543-
:::warning
544-
**Window Function Performance Considerations:**
545-
546-
1. **Index Your Ordered Columns**
547-
```sql
548-
-- If you frequently order by sale_date:
549-
CREATE INDEX idx_sales_date ON sales(sale_date);
550-
551-
-- For partitioned queries:
552-
CREATE INDEX idx_sales_dept_date ON sales(department, sale_date);
553-
```
554-
555-
2. **Use OVER () Wisely**
556-
```sql
557-
-- Bad: Recalculating same window multiple times
558-
SELECT
559-
SUM(amount) OVER (PARTITION BY dept ORDER BY date),
560-
AVG(amount) OVER (PARTITION BY dept ORDER BY date),
561-
COUNT(*) OVER (PARTITION BY dept ORDER BY date)
562-
FROM sales;
563-
564-
-- Good: Use WINDOW clause (PostgreSQL)
565-
SELECT
566-
SUM(amount) OVER w,
567-
AVG(amount) OVER w,
568-
COUNT(*) OVER w
569-
FROM sales
570-
WINDOW w AS (PARTITION BY dept ORDER BY date);
571-
572-
-- Or use CTE to calculate once
573-
WITH dept_totals AS (
574-
SELECT dept, SUM(amount) as total
575-
FROM sales
576-
GROUP BY dept
577-
)
578-
SELECT s.*, dt.total
579-
FROM sales s
580-
JOIN dept_totals dt ON s.dept = dt.dept;
581-
```
582-
583-
3. **Limit Your Windows**
584-
```sql
585-
-- Avoid if possible: UNBOUNDED FOLLOWING forces scanning entire partition
586-
LAST_VALUE(x) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
587-
588-
-- Better: Use subquery if you need the last value
589-
```
590-
591-
4. **Filter Before Windowing**
592-
```sql
593-
-- Good: Filter reduces rows before window function
594-
SELECT
595-
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) as rn,
596-
*
597-
FROM employees
598-
WHERE hire_date >= '2020-01-01' -- Filter first
599-
AND status = 'Active';
600-
```
601-
:::
602-
603-
## Common Mistakes to Avoid
604-
605-
:::danger
606-
**Pitfall #1: Wrong Frame with LAST_VALUE**
607-
```sql
608-
-- Wrong: This gives current row, not last row!
609-
LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY hire_date)
610-
611-
-- Correct: Need to specify the full frame
612-
LAST_VALUE(salary) OVER (
613-
PARTITION BY dept
614-
ORDER BY hire_date
615-
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
616-
)
617-
```
618-
619-
**Pitfall #2: Using WHERE with Window Functions**
620-
```sql
621-
-- Wrong: Window functions can't be in WHERE clause
622-
SELECT * FROM sales
623-
WHERE ROW_NUMBER() OVER (ORDER BY amount) <= 10;
624-
625-
-- Correct: Use CTE or subquery
626-
WITH ranked AS (
627-
SELECT *, ROW_NUMBER() OVER (ORDER BY amount DESC) as rn
628-
FROM sales
629-
)
630-
SELECT * FROM ranked WHERE rn <= 10;
631-
```
632-
633-
**Pitfall #3: Forgetting ORDER BY for Sequential Functions**
634-
```sql
635-
-- Wrong: LAG without ORDER BY is meaningless
636-
LAG(amount) OVER (PARTITION BY customer_id)
637-
638-
-- Correct: Must specify order
639-
LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date)
640-
```
641-
642-
**Pitfall #4: PARTITION BY vs GROUP BY Confusion**
643-
```sql
644-
-- GROUP BY: Collapses rows
645-
SELECT department, COUNT(*), AVG(salary)
646-
FROM employees
647-
GROUP BY department; -- Returns one row per department
648-
649-
-- PARTITION BY: Keeps all rows
650-
SELECT
651-
employee_name,
652-
department,
653-
salary,
654-
AVG(salary) OVER (PARTITION BY department) as dept_avg
655-
FROM employees; -- Returns every employee with dept average
656-
```
657-
:::
658417

659418
## Window Functions Quick Reference
660419

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const sidebars: SidebarsConfig = {
151151
items: [
152152
'sql/SQL-Advance/sql-subqueries',
153153
'sql/SQL-Advance/common-table-expressions',
154+
'sql/SQL-Advance/window-functions',
154155
],
155156
},
156157
],

0 commit comments

Comments
 (0)