Day 9: Window Functions — Analytics Without Aggregation



This content originally appeared on DEV Community and was authored by Hardik Kanajariya

Day 9: Window Functions — Analytics Without Aggregation

Welcome to Day 9! 📊

Today we’ll unlock the power of window functions to compute rankings, running totals, moving averages, and more — all without collapsing rows like GROUP BY does.

What You’ll Learn

  1. OVER() and PARTITION BY basics
  2. Ranking functions: ROW_NUMBER, RANK, DENSE_RANK
  3. Value functions: LAG, LEAD, FIRST_VALUE, LAST_VALUE
  4. Aggregates as windows: SUM, AVG, COUNT with frames
  5. Frame clauses: ROWS/RANGE and window frames
  6. Practical analytics patterns

1) OVER() and PARTITION BY

-- Average salary by department, shown on each row
SELECT 
  employee_id,
  department,
  salary,
  AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;

2) Ranking Functions

-- Rank employees by salary per department
SELECT 
  employee_id, department, salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
  RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rnk
FROM employees;

When duplicates occur, RANK leaves gaps, DENSE_RANK does not; ROW_NUMBER is unique per partition.

3) Value Navigation: LAG / LEAD

-- Compare each month to previous
SELECT 
  DATE_TRUNC('month', order_date) AS month,
  SUM(total_amount) AS revenue,
  LAG(SUM(total_amount))  OVER (ORDER BY DATE_TRUNC('month', order_date)) AS prev_rev,
  LEAD(SUM(total_amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)) AS next_rev
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

4) Running Totals and Moving Averages

-- Running total revenue per customer
SELECT 
  customer_id,
  order_date,
  total_amount,
  SUM(total_amount) OVER (
    PARTITION BY customer_id 
    ORDER BY order_date 
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM orders;

-- 3-month moving average of revenue
WITH monthly AS (
  SELECT DATE_TRUNC('month', order_date) AS month,
         SUM(total_amount) AS revenue
  FROM orders
  GROUP BY DATE_TRUNC('month', order_date)
)
SELECT month,
       revenue,
       AVG(revenue) OVER (
         ORDER BY month
         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ) AS mov_avg_3m
FROM monthly
ORDER BY month;

5) FIRST_VALUE, LAST_VALUE, NTH_VALUE

SELECT 
  department,
  employee_id,
  salary,
  FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS top_salary,
  LAST_VALUE(salary)  OVER (PARTITION BY department ORDER BY salary DESC 
                            ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS bottom_salary
FROM employees;

Note: LAST_VALUE needs an explicit frame to look ahead.

6) Practical Patterns

  • Top-N per group (filter rn <= N)
  • Churn/retention using LAG dates
  • Cohort performing metrics over time
-- Top 3 products by revenue per category
WITH product_rev AS (
  SELECT 
    category,
    product_id,
    SUM(quantity * unit_price) AS revenue
  FROM sales
  GROUP BY category, product_id
)
SELECT * FROM (
  SELECT 
    category,
    product_id,
    revenue,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn
  FROM product_rev
) t
WHERE rn <= 3
ORDER BY category, revenue DESC;

Performance Tips

  • Add ORDER BY indexes for large window sorts
  • Prefer ROWS frames for deterministic behavior
  • Use EXPLAIN ANALYZE to understand sort and window costs

Today’s Challenge 🎯

1) Compute each customer’s running total and flag when they cross $5,000 lifetime spend
2) For each department, list top 2 salaries and the gap from the previous salary
3) Build a 6-month moving average on monthly revenue

-- Your solutions here

Summary

  • Window functions add analytics without collapsing rows
  • Ranking, navigation, and aggregate windows cover most use cases
  • Frames control the window — choose wisely

Coming Up Next

Day 10: Indexes and Performance Optimization — speed up your queries! #PostgreSQL #SQL #WindowFunctions #Analytics #15DaysOfPostgreSQL


This content originally appeared on DEV Community and was authored by Hardik Kanajariya