@@ -414,247 +414,6 @@ N FOLLOWING -- N rows after current
414
414
</TabItem>
415
415
</Tabs>
416
416
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
- :::
658
417
659
418
## Window Functions Quick Reference
660
419
0 commit comments