Sales organizations run on cadence: monthly quotas, quarterly targets, year-over-year growth commitments to the board. Almost every metric a sales leader looks at is a comparison over time or a ranking against peers — and both are core window function use cases.
This chapter shifts focus from the grouped comparisons of HR Analytics (Chapter 01) to time-based comparisons: running totals, moving averages, and period-over-period growth. These patterns power nearly every revenue dashboard in existence.
A typical sales schema centers on a transactional fact table:
sales (sale_id, salesperson_id, region_id, sale_date, revenue, ...)salespeople (salesperson_id, salesperson_name, region_id, ...)regions (region_id, region_name, ...)Sales leadership consumes this data primarily through rollups over time (daily, monthly, quarterly, yearly) and rankings across people or regions.
Time-based sales metrics require a row to “see” other rows around it in time - the previous month’s revenue (for MoM growth), the same month last year (for YoY growth), or a rolling window of preceding days (for a moving average) - all while keeping every period as its own row for charting. This is structurally identical to the HR “compare to peers” problem, except the partition and order are now built around time instead of department. LAG() becomes the primary tool for period-over-period growth, and framed aggregate windows (ROWS BETWEEN ...) become essential for running totals and moving averages.
| Function | Business Explanation |
|---|---|
RANK() / DENSE_RANK() |
Leaderboards for salespeople and regions, with explicit tie-handling semantics. |
SUM() OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) |
Running (cumulative) revenue toward a target. |
AVG() OVER (... ROWS BETWEEN N PRECEDING AND CURRENT ROW) |
Moving average revenue for trend smoothing. |
LAG() |
Retrieves prior period’s revenue for MoM / YoY growth calculations. |
DATEDIFF / date arithmetic with LAG() |
Computes the gap between consecutive sales for velocity analysis. |
ROWS BETWEEN ... PRECEDING AND CURRENT ROW) and why the default frame (when ORDER BY is present but no frame is stated) can silently produce a running total instead of the full-partition total you may have expected.LAG() with an offset of 12 (LAG(revenue, 12)) to fetch “the same month, one year ago” from a monthly-grain table.NULLIF().sale_date (or the relevant order column) is indexed alongside the partition key, e.g., (salesperson_id, sale_date).ROWS BETWEEN N PRECEDING AND CURRENT ROW frame is generally cheaper to compute than a RANGE-based frame, because ROWS operates on a fixed physical row count rather than re-evaluating value-based boundaries.SUM() OVER (PARTITION BY ... ORDER BY ...) always returns the full partition total - by default, with an ORDER BY present, most engines apply an implicit running-total frame.LAG(revenue, 12) over a monthly grain, leading to fragile, hard-to-maintain SQL.RANK() for a leaderboard display where tied reps unexpectedly cause the next several ranks to be skipped, confusing a dashboard consumer expecting sequential ranks.SUM(revenue) OVER (PARTITION BY salesperson_id ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).LAG(revenue) OVER (ORDER BY month) followed by a percentage-change calculation with NULLIF protection.LAG(revenue, 12) rather than LAG(revenue, 1).ROWS frame and a RANGE frame in a window function?” — Expect an explanation that ROWS counts physical rows while RANGE operates on logical value ranges, which matters when ORDER BY values contain duplicates.AVG(revenue) OVER (ORDER BY sale_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW).Sales analytics reframes the window function toolkit around time rather than category. Running totals, moving averages, and lagged period comparisons are the backbone of every revenue dashboard - and once you can build them fluently, the same patterns transfer directly to finance and e-commerce reporting.
LAG() to compute the number of days between consecutive deals per salesperson, and flag reps with unusually long gaps.Next: 02_SALES_ANALYTICS.sql — the fully engineered SQL chapter for this domain.
Previous: 01_HR_ANALYTICS.md · Module: README · Next chapter: 03_ECOMMERCE.md