@@ -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
0 commit comments